STM32F407(STM32F4-DISCOVERY) - Abordare non-standard - Bibliotecă standard partea 1. Conectarea unei biblioteci periferice standard la orice familie STM32

Până în acest moment, am folosit biblioteca standard de kernel - CMSIS. Pentru a configura un port în modul de operare dorit, a trebuit să apelăm la pentru a găsi registrul responsabil pentru o anumită funcție și, de asemenea, să căutăm printr-un document mare și 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 cum 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 bibliotecă 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 corespunde a două fișiere - antet (*.h) și codul sursă (*.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 periferic. Î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. Constanțele 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 de module RTC și GPIO (cu toate acestea, în viitor vom 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 completați-o GPIO_InitTypeDef LED; LED.GPIO_Pin = GPIO_Pin_0; LED.GPIO_Model_GPIO_Speed_2MHIO = GPIO_Speed_Speed. 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.

M-am dus din nou drumul lung- am generat spațiile corespunzătoare în STM32CubeMX, 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țializează pa12 pin gpio_inittipedef pininit; pininit.pin = gpio_pin_12; pininit.mode = gpio_mode_output_pp; pininit.speed = gpio_speed_freq_low; hal_gpio_init (gpioa, & pininit); 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 a 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 minimum suma necesară cod pentru a implementa piesele interfeței arduino de care am 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 locuiește în STM32GENERIC și pare să fie mai confortabil acolo. Desigur, a fost posibil să tăiați toate dependențele și să vă scrieți propriile clase de wrapper pentru sarcinile dvs., dar acest lucru m-ar fi încetinit pentru î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 cea mai mare parte a Manualului de referință STM32, m-am uitat la modul în care au fost realizate bibliotecile HAL și învelișurile STM32GENERIC și am avansat în înțelegerea descriptorilor USB și a 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 intră î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, I2C_AD_AD_16<<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 este î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 a pinului 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. e = mod; GP IO_InitStruct. Pull = trage; GPIO_InitStruct.Speed ​​​​= GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(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. Se pare că 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 ridicarea liniei 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) ); /* Trageți jos */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); /* Eliberați din nou ridicat */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); ) /* Generați condiția de pornire și oprire */ gpio_write_bit(sda_port(dev), dev->sda_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); gpio_write_bit(sda_port(dev), dev->sda_pin, 1); )

Din nou: UART

M-am bucurat să mă întorc î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. Ca urmare, singura modalitate de a obține date este prin sondaj constant. Desigur, am adăugat vTaskDelay(10) la handlerul GPS pentru a reduce încărcătura măcar puțin, dar în realitate aceasta este o cârjă.

Primul gând, desigur, a fost să atașez DMA. Ar merge 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 timpul procesorului salvând linia primită î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, aceasta 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(); // Inițiază pinii în modul de funcționare alternativ GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; // Pin TX GPIO_InitStruct.Mode = GPIOPP_Struct.MODE_GPIOPP_SGPIO_Struct. PEED_FREQ_ HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct .Pin = GPIO_PIN_10; //pin RX GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // Init uart =Handle.Baud.1RaitStruct; ; uartHand le.Init. WordLength = UART_WORDLENGTH_8B; uartHandle.Init.StopBits = UART_STOPBITS_1; uartHandle.Init.Parity = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.CONTROL =HwFONE; 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 sa este doar să apeleze handler-ul din HAL. A doua funcție este „callback” de la HAL că 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 gestionarea întreruperilor. 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 utilizat ulterior în scopuri de sincronizare gpsUart.init(); pentru (;;) ( // Așteptați până când întregul șir este primit dacă(!gpsUart.waitForString()) continuă; // Citiți șirul primit și analizați fluxul GPS char cu caracter în timp ce(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.sat_count ); ) 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 specificat. Î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 protocolul de nivel superior în aproape același mod. 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 ifdefs. Ca urmare, s-a dovedit a fi o mizerie de cod pentru suportarea diferitelor interfețe. Așa că, înainte de a merge mai departe, a fost necesar să-l aranjezi.

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ă se adauge 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 cu adevărat binevenită aici.

Mai întâi, să inițializam DMA. Dorim 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 designului. Î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) (afișare)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 dvs. 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); virtual void send(const uint8_t* buf, size_t n); virtual void select(); virtual void setSpiSettings(SPISettings spiSettings) ); virtual void unselect(); );


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-ului GPS xSDThread = xTaskGetCurrentTaskHandle(); // Enable_KHAI_CALL_CLOPERHANDLE(); () ; __HAL_RCC_SPI1_CLK_ENABLE(); // Pini de inițiere GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7; //MOSI & SCK GPIO_Init_InitStruct_Struct.SGP_MODE_InitStruct. = GPIO_SPE ED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_6; //MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Mod. MODE_OUTPUT_PP; GP IO_InitStruct.Speed ​​​​= GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init (GPIOA, &GPIO_InitStruct); // Setați pinul CS High în mod implicit HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // Inițiază 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 a fi 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); HAL_SPI_Receive(&spiHandle, buf, n, 10); return 0; ) void SdFatSPIDriver::send(uint8_t data) ( HAL_SPI_Transmit(&spiHandle, &data, 1, 10); ) 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); 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().hdmarx);(hAL_DMA_IRQHandler()._getHdHandle_SpiDri). Tx CpltCallback (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 utilizare. 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 numai 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ă numai gazda poate iniția transferul în protocolul USB. 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 acest lucru. Î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 se plimbase cu așa ceva. Iată codul pe care l-am reproiectat 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 învecinate până la sfârșitul buffer-ului dacă (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count = sizeof(usbTx)Tail;)_FSC_Transmit (&usbTxBuffer, număr); 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) return /; / Transmite mesajul, dar nu mai mult de timeout uint32_t timeout = HAL_GetTick() + 5; // Protejează această funcție de intrări multiple 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

uint8_t USBD_CDC_DataIn static (USBD_HandleTypeDef *pdev, uint8_t epnum) ( USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; ; USBSerialTransferCompleted CB();return USBD_OK ; ) altfel ( returnează 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 procentual // // 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.
  • Setters și getters de nivel ușor mai scăzut 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); LL_GPIO_SetPinSpeed(GPIOC, pin, LL_GPIO_) (GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); Pin de ieșire (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 doar 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 chestiile 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, MOLL_GPIOCPUll_, LLL_GPIOTPull, LL_GPIOCPUll_; 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); ) // Citirea stării butonului (efectuați mai întâi deboucul) inline bool getButtonState(uint32_GPPIO_PIN_, (if) (if_GPIO_Input_) // dob uncie vTaskDelay( DEBOUNCE_DURATION ); if(LL_GPIO_IsInputPinSet(GPIOC, pin)) returnează adevărat; ) 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 driverului 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 un 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ță excesului de cod în flash folosind optimizarea timpului de legătură, atunci consumul imens de RAM poate fi vindecat doar prin rescrierea lui în LL HAL.

Dar nici măcar asta nu este deranjant, 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 de descriptori și alte lucruri în flash. 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 indicatoare 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 „tipul Unicode”, care ar putea fi făcută în timp de compilare. Mai mult, un buffer special este alocat pentru aceasta

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 ; în timp ce (*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 demn 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
Butoanele 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

Software-ul necesar pentru dezvoltare. În acest articol vă voi spune cum să îl configurați și să îl conectați corect. Toate mediile comerciale precum IAR EWARM sau Keil uVision realizează de obicei această integrare singure, dar în cazul nostru totul va trebui configurat manual, petrecând mult timp pe ea. Avantajul este că aveți șansa de a înțelege cum funcționează totul din interior și, în viitor, să personalizați totul în mod flexibil pentru dvs. Înainte de a începe configurarea, să ne uităm la structura mediului în care vom lucra:

Eclipse va fi folosit pentru a edita în mod convenabil fișierele de implementare a funcției ( .c), fișiere antet ( .h), precum și fișierele de asamblare ( .S). Prin „convenient” mă refer la utilizarea completării codului, evidențierea sintaxei, refactorizarea, navigarea prin funcții și prototipurile acestora. Fișierele sunt transmise automat compilatoarelor necesare, care generează cod obiect (în fișiere .o). Până acum acest cod nu conține adrese absolute variabile și funcții și, prin urmare, nu sunt potrivite pentru execuție. Fișierele obiect rezultate sunt asamblate împreună de un linker. Pentru a ști ce părți din spațiul de adrese să folosească, colectorul folosește un fișier special ( .ld), care se numește script linker. De obicei, conține o definiție a adreselor secțiunilor și a dimensiunilor acestora (secțiunea de cod mapată la flash, secțiune variabilă mapată la RAM etc.).

În cele din urmă, linker-ul generează un fișier .elf (Executable and Linkable Format), care conține, pe lângă instrucțiuni și date, informații de depanare utilizate de depanator. Acest format nu este potrivit pentru intermiterea obișnuită a firmware-ului cu programul vsprog, deoarece aceasta necesită un fișier imagine de memorie mai primitiv (de exemplu, Intel HEX - .hex). Pentru a-l genera, există și un instrument din setul Sourcery CodeBench (arm-none-eabi-objcopy) și se integrează perfect în eclipse folosind plugin-ul ARM instalat.

Pentru a efectua depanarea în sine, sunt utilizate trei programe:

  1. eclipse în sine, care permite programatorului să folosească „vizual” depanarea, să treacă prin linii, să treacă cu mouse-ul peste variabile pentru a le vedea valorile și alte facilități
  2. arm-none-eabi-gdb - Clientul GDB este un depanator care este controlat ascuns de eclips (prin stdin) ca răspuns la acțiunile specificate la pasul 1. La rândul său, GDB se conectează la serverul OpenOCD Debug și toate comenzile de intrare sunt traduse de depanatorul GDB în comenzi ușor de înțeles pentru OpenOCD. Canalul GDB<->OpenOCD este implementat folosind protocolul TCP.
  3. OpenOCD este un server de depanare care poate comunica direct cu programatorul. Acesta rulează în fața clientului și așteaptă o conexiune TCP.

Această schemă vă poate părea destul de inutilă: de ce să folosiți separat clientul și serverul și să efectuați traduceri inutile de comenzi, dacă toate acestea ar putea fi făcute cu un singur depanator? Faptul este că o astfel de arhitectură permite teoretic un schimb convenabil de client și server. De exemplu, dacă trebuie să utilizați un alt programator în loc de versaloon, care nu va suporta OpenOCD, dar va suporta un alt server special de depanare (de exemplu, texane/stlink pentru programatorul stlink - care se află în placa de depanare STM32VLDiscovery), atunci veți rula pur și simplu OpenOCD în loc să lansați serverul necesarși totul ar trebui să funcționeze, fără mișcări suplimentare. În același timp, este posibilă situația inversă: să presupunem că ați vrut să utilizați mediul IAR EWARM împreună cu versaloon în loc de combinația Eclipse + CodeBench. IAR are propriul client Debug încorporat, care va contacta cu succes OpenOCD și îl va gestiona, precum și va primi datele necesare ca răspuns. Totuși, toate acestea uneori rămân doar în teorie, deoarece standardele de comunicare între client și server nu sunt strict reglementate și pot diferi pe alocuri, dar configurațiile pe care le-am specificat cu st-link+eclipse și IAR+versaloon au funcționat pentru mine.

De obicei, clientul și serverul rulează pe aceeași mașină și conexiunea la server are loc la localhost:3333(Pentru openocd), sau localhost:4242(pentru texan/stlink st-util). Dar nimeni nu vă împiedică să deschideți portul 3333 sau 4242 (și să redirecționați acest port pe router către rețea externă) și colegii tăi din alt oraș se vor putea conecta și depana hardware-ul tău. Acest truc este adesea folosit de embeders care lucrează pe site-uri la distanță unde accesul este limitat.

Să începem

Lansați eclipse și selectați File->New->C Project, selectați tipul de proiect ARM Linux GCC (Sorcery G++ Lite) și numele „stm32_ld_vl” (Dacă aveți STV32VLDiscovery, atunci ar fi mai logic să-l numiți „stm32_md_vl”) :

Faceți clic pe Terminare și minimizați sau închideți fereastra Bun venit. Deci, proiectul a fost creat și folderul stm32_ld_vl ar trebui să apară în spațiul dvs. de lucru. Acum trebuie să fie umplut cu bibliotecile necesare.

După cum înțelegeți din numele proiectului, voi crea un proiect pentru vizualizarea riglă linie de valoare de joasă densitate(LD_VL). Pentru a crea un proiect pentru alte microcontrolere trebuie să înlocuiți toate fișierele și să definiți în numele cărora există _LD_VL (sau_ld_vl) la cele de care aveți nevoie, conform tabelului:

Tip riglă Desemnare Microcontrolere (x se poate schimba)
Linie de valoare cu densitate scăzută _LD_VL STM32F100x4 STM32F100x6
Densitate scazuta _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Linie de valoare de densitate medie _MD_VL STM32F100x8 STM32F100xB
Densitate medie
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
Linie de valoare de înaltă densitate _HD_VL STM32F100xC STM32F100xD STM32F100xE
Densitate mare _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
XL-densitate _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Linie de conectivitate _CL STM32F105xx și STM32F107xx

Pentru a înțelege logica din spatele mesei, trebuie să fiți familiarizat cu etichetarea STM32. Adică, dacă aveți VLDiscovery, atunci va trebui să înlocuiți tot ce este legat de _LD_VL cu _MD_VL, deoarece cipul STM32F100RB, care aparține liniei de valoare cu densitate medie, este lipit în descoperire.

Adăugarea bibliotecii de periferice standard CMSIS și STM32F10x la proiect

CMSIS(Cortex Microcontroller Software Interface Standard) este o bibliotecă standardizată pentru lucrul cu microcontrolere Cortex care implementează nivelul HAL (Hardware Abstraction Layer), adică vă permite să faceți abstracție din detaliile lucrului cu registre, căutând adrese de registre folosind foile de date, etc. Biblioteca este un set de coduri sursă în C și Asm. Partea de bază a bibliotecii este aceeași pentru toate Cortex (Fie că este ST, NXP, ATMEL, TI sau oricine altcineva) și este dezvoltată de ARM. Cealaltă parte a bibliotecii este responsabilă de periferie, care diferă în mod natural diferiți producători. Prin urmare, în cele din urmă, biblioteca completă este încă distribuită de producător, deși partea de kernel poate fi încă descărcată separat de pe site-ul web ARM. Biblioteca conține definiții de adrese, cod de inițializare a generatorului de ceas (personalizat convenabil prin definiții) și orice altceva care îl scutește pe programator de a introduce manual în proiectele sale definiția adreselor de tot felul de registre periferice și de a determina biții valorilor lui. aceste registre.

Dar băieții de la ST au mers mai departe. Pe lângă suportul CMSIS, oferă o altă bibliotecă pentru STM32F10x numită Biblioteca de periferice standard(SPL), care poate fi utilizat pe lângă CMSIS. Biblioteca oferă mai rapid și acces usor la periferie și, de asemenea, controlează (în unele cazuri) funcționarea corectă a periferiei. De aceea datele bibliotecii numit adesea un set de drivere pentru module periferice. Este însoțit de un pachet de exemple, împărțit în categorii pentru diferite periferice. Biblioteca este disponibilă nu numai pentru STM32F10x, ci și pentru alte serii.

Puteți descărca întreaga versiune SPL+CMSIS 3.5 aici: STM32F10x_StdPeriph_Lib_V3.5.0 sau pe site-ul ST. Dezarhivați arhiva. Creați folderele CMSIS și SPL în folderul proiectului și începeți să copiați fișierele în proiect:

Ce să copiezi

Unde să copiați (având în vedere
că folderul proiectului este stm32_ld_vl)

descrierea fisierului
Biblioteci/CMSIS/CM3/
CoreSupport/ core_cm3.c
stm32_ld_vl/CMSIS/ core_cm3.c Descrierea nucleului Cortex M3
Biblioteci/CMSIS/CM3/
CoreSupport/ miez_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h Anteturi de descriere a nucleului

ST/STM32F10x/ system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c Funcții de inițializare și
controlul ceasului
Biblioteci/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h Anteturi pentru aceste funcții
Biblioteci/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h Descrierea de bază a perifericelor
Biblioteci/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/startup/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! Atenție extensie de fișier CAPITAL S)
Fișier de masă vectorială
întrerupe și init-s pe asm
Proiect/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/ stm32f10x_conf.h Șablon pentru personalizare
module periferice

inc/ *
stm32_ld_vl/SPL/inc/ * Fișiere antet SPL
Biblioteci/STM32F10x_StdPeriph_Driver/
src/ *
stm32_ld_vl/SPL/src/ * Implementarea SPL

După copiere, accesați Eclipse și faceți Refresh în meniul contextual al proiectului. Ca rezultat, în Project Explorer ar trebui să obțineți aceeași structură ca în imaginea din dreapta.

Poate ați observat că în folderul Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ există foldere pentru diferite IDE-uri (diferite IDE-uri folosesc compilatoare diferite). Am ales Ride7 IDE pentru că folosește compilatorul GNU Instrumente pentru ARM Embedded, compatibil cu Sourcery CodeBench.

Întreaga bibliotecă este configurată folosind un preprocesor (folosind defines), acest lucru vă va permite să rezolvați toate ramurile necesare în etapa de compilare (sau mai bine zis, chiar înainte de aceasta) și să evitați încărcarea în funcționarea controlerului în sine (care ar fi observat dacă configurarea a fost efectuată în RunTime). De exemplu, toate echipamentele sunt diferite pentru linii diferite și, prin urmare, pentru ca biblioteca să „știe” ce linie doriți să utilizați, vi se cere să anulați comentariile în fișier stm32f10x.h una dintre definiții (corespunzătoare liniei dvs.):

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

Și așa mai departe...

Dar nu recomand să faci asta. Nu vom atinge fișierele bibliotecii deocamdată și le vom defini mai târziu folosind setările compilatorului din Eclipse. Și apoi Eсlipse va apela compilatorul cu cheia -D STM32F10X_LD_VL, care pentru preprocesor este absolut echivalent cu situația dacă ați decomentat „#define STM32F10X_LD_VL”. Astfel, nu vom schimba codul; ca urmare, dacă doriți, într-o zi veți putea muta biblioteca într-un director separat și nu o copiați în folderul fiecărui proiect nou.

Script de linker

În meniul contextual al proiectului, selectați Nou->Fișier->Altele->General->Fișier, Următorul. Selectați folderul rădăcină al proiectului (stm32_ld_vl). Introduceți numele fișierului „stm32f100c4.ld” (sau „stm32f100rb.ld” pentru descoperire). Acum copiați și inserați în eclipse:

ENTRY(Reset_Handler) MEMORY ( FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K ) _stack = ORIGIN(RAM) + LENGTH(RAM); MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SECȚIUNI ( /* Întreruperea tabelului vectorial */ .isr_vector: ( . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); ) >FLASH /* Codul programului și alte date intră în FLASH * / .text: ( . = ALIGN(4); /* Cod */ *(.text) *(.text*) /* Constante */ *(.rodata) *(.rodata*) /* ARM->Thumb și Thumb->ARM glue code */ *(.glue_7) *(.glue_7t) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; ) >FLASH . ARM.extab: ( *(.ARM.extab* .gnu.linkonce.armextab.*) ) >FLASH .ARM: ( __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; ) >FLASH .ARM. atribute: ( *(.ARM.attributes) ) > FLASH .preinit_array: ( PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); ) > PROVIDE_FLASH:_IT_HID:_it_it . = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); ) >FLASH .fini_array: ( PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*); (.fini_array*)) KEEP (*(SORT(.fini_array.*))) PROVIDE_HIDDEN (__fini_array_end = .); ) >FLASH_sidata = .; /* Date inițiale */ .data: AT (_sidata) ( . = ALIGN(4); _sdata = .; /* creează un simbol global la începutul datelor */ *(.data) *(.data*) . = ALIGN (4); _edata = .; /* definește un simbol global la sfârșitul datelor */ ) >RAM /* Date neinițializate */ . = ALIGN(4); .bss: ( /* Acesta este folosit de pornire pentru a inițializa secțiunea .bss */ _sbss = .; /* definește un simbol global la începutul bss */ __bss_start__ = _sbss; *(.bss) *(.bss *) *(COMUN) .= ALIGN(4); _ebss = .; /* definește un simbol global la sfârșitul bss */ __bss_end__ = _ebss; ) >RAM PROVIDE(end = _ebss); PROVIDE(_end = _ebss); PROVIDE(__HEAP_START = _ebss); /* Secțiunea User_heap_stack, folosită pentru a verifica dacă mai există suficientă memorie RAM */ ._user_heap_stack: ( . = ALIGN(4); . = . + MIN_HEAP_SIZE; . = . + MIN_STACK_SIZE; . = ALIGN(4); ) >RAM / DISCARD/ : ( libc.a(*) libm.a(*) libgcc.a(*) ) )

Acest l Scriptul de cerneală va fi destinat special controlerului STM32F100C4 (care are 16 KB de flash și 4 KB de RAM), dacă aveți unul diferit, va trebui să modificați parametrii LUNGIME ai zonelor FLASH și RAM la începutul fișierul (pentru STM32F100RB, care în Discovery: Flash 128K și RAM 8K).

Salvați fișierul.

Configurare build (build C/C++)

Accesați Proiect->Proprietăți->C/C++ Build->Settings->Tool Settings și începeți configurarea instrumentelor de compilare:

1) Precesor țintă

Selectăm pentru ce nucleu Cortex va funcționa compilatorul.

  • Procesor: cortex-m3

2) ARM Sourcery Linux GCC C Compiler -> Preprocesor

Adăugăm două definiții trecându-le prin comutatorul -D către compilator.

  • STM32F10X_LD_VL - definește rigla (am scris despre această definiție mai sus)
  • USE_STDPERIPH_DRIVER - spune bibliotecii CMSIS că ar trebui să folosească driverul SPL

3) ARM Sourcery Linux GCC C Compiler -> Directoare

Adăugați căi la bibliotecă inclusiv.

  • „$(workspace_loc:/$(ProjName)/CMSIS)”
  • „$(workspace_loc:/$(ProjName)/SPL/inc)”

Acum, de exemplu, dacă scriem:

#include „stm32f10x.h

Apoi compilatorul trebuie să caute mai întâi fișierul stm32f10x.hîn directorul de proiect (face mereu asta), nu îl va găsi acolo și va începe să caute în folderul CMSIS, calea către care am indicat, și o va găsi.

4) ARM Sourcery Linux GCC C Compiler -> Optimizare

Să activăm funcțiile și optimizarea datelor

  • -secţiuni-funcţii
  • -fdata-secțiuni

Ca urmare, toate funcțiile și elementele de date vor fi plasate în secțiuni separate, iar colectorul va putea înțelege care secțiuni nu sunt folosite și pur și simplu le va arunca.

5) ARM Sourcery Linux GCC C Compiler -> General

Adăugați calea către scriptul nostru de linker: „$(workspace_loc:/$(ProjName)/stm32f100c4.ld)” (sau cum o numiți).

Și setați opțiunile:

  • Nu utilizați fișiere de pornire standard - nu utilizați fișiere de pornire standard.
  • Eliminați secțiunile neutilizate - eliminați secțiunile neutilizate

Gata, configurarea este completă. BINE.

Am făcut multe de când a fost creat proiectul și există câteva lucruri pe care Eclipse ar fi putut să le fi omis, așa că trebuie să-i spunem să reconsidere structura fișierului proiectului. Pentru a face asta de la meniul contextual proiectul trebuie realizat Index -> reconstruire.

Salut LED-uri pe STM32

Este timpul să creați fisierul principal proiect: Fișier -> Nou -> C/C++ -> Fișier sursă. Următorul. Nume fișier Fișier sursă: main.c.

Copiați și inserați următoarele în fișier:

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // Activați ceasul periferic PORTB RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Activați ceasul periferic TIM2 // Dezactivați JTAG pentru eliberarea LED-ului PIN RCC->APB1ENR |= RAFCC_APR2EN;>APB1ENR_TIM2EN; AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Ștergeți biții registrului de control PB4 și PB5 GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_MODE5 | GPIO_CRL_CNF5. /4 Push/ Configurați PB5. max. 10Mhz GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec TIM2->ARR = 1000; // 1 Interrupt TIM2->_; 1 sec. // Activează întreruperea tim2 TIM2->CR1 |= TIM_CR1_CEN; // Începe numărul NVIC_EnableIRQ(TIM2_IRQn); // Activează IRQ while(1); // Buclă infinită ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UI ; //Curățați flag UIF dacă (1 == (i++ & 0x1)) ( GPIOB->BSRR = GPIO_BSRR_BS4; // Setați bitul PB4 GPIOB->BSRR = GPIO_BSRR_BR5; // Resetați bitul PB5 ) altfel ( GPIOB->BSRR = GPIO_BSRR_BS5; // Setați PB5 bit GPIOB->BSRR = GPIO_BSRR_BR4; // Resetează bitul PB4 ) )

Deși am inclus biblioteca SPL, aceasta nu a fost folosită aici. Toate apelurile către câmpuri precum RCC->APB2ENR sunt descrise complet în CMSIS.

Puteți face Project -> Build All. Dacă totul a funcționat, atunci fișierul stm32_ld_vl.hex ar trebui să apară în folderul Debug al proiectului. A fost generat automat de la elf de instrumentele încorporate. Am intermitent fișierul și vedem cum LED-urile clipesc la o frecvență de o dată pe secundă:

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

Desigur, în loc de /home/user/workspace/ trebuie să introduceți calea către spațiul de lucru.

Pentru STM32VLDiscovery

Codul este ușor diferit de cel pe care l-am dat mai sus pentru placa mea de depanare. Diferența constă în pinii de care „atârnă” LED-urile. Dacă pe placa mea erau PB4 și PB5, atunci pe Discovery erau PC8 și PC9.

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Activare PORTC Periph Clock RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Activare TIM2 Periph Clock // Șterge biții registrului de control PC8 și PC9 &= GPIOC->CR (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Configurați PC8 și PC9 ca ieșire Push Pull la maxim 10Mhz GPIOC->CRH |= GPIO_CRH_MODE8_0 | GPIO_CRH_CNF9; GPIO_CRH_0 = Sistem de blocare /MODE29_CRH_010-10Mhz ; // 1000 tick/sec TIM2->ARR = 1000; // 1 întrerupere/sec (1000/100) TIM2->DIER |= TIM_DIER_UIE; // Activare întrerupere tim2 TIM2->CR1 |= TIM_CR1_CEN; // Pornire numărătoare NVIC_EnableIRQ(TIM2_IRQn); // Activați IRQ while(1); // Buclă infinită ) void TIM2_IRQHandler (void) ( TIM2->SR &= ~TIM_SR_UIF; // Curățați flag UIF dacă (1 == (i++ & 0x1)) ( GPIOC->BSRR = GPIO_BSRR_BS8 ; // Setați bitul PC8 GPIOC->BSRR = GPIO_BSRR_BR9; // Resetați bitul PC9 ) altfel ( GPIOC->BSRR = GPIO_BSRR_BS9; // Setați bitul PC9 GPIOC->BSRR = GPIO_BSRR_BR8; // Resetați bitul PC8 ) )

În Windows, puteți să flashați hexadecimalul rezultat(/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) folosind utilitarul de la ST.

Ei bine, sub utilitar linux st-flash. DAR!!! Utilitarul nu folosește formatul Intel HEX hex (care este generat implicit), așa că este extrem de important să selectați formatul binar în setările de creare a imaginii Flash:

Extensia fișierului nu se va modifica (va rămâne hex așa cum a fost), dar formatul fișierului se va schimba. Și numai după aceea poți face:

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

Apropo, în ceea ce privește extensia și formatul: de obicei fișierele binare sunt marcate cu extensia .bin, în timp ce fișierele în format Intel HEX se numesc extensia .hex. Diferența dintre aceste două formate este mai mult tehnică decât funcțională: formatul binar conține pur și simplu octeți de instrucțiuni și date care vor fi pur și simplu scrise în controler de către programator „ca atare”. IntelHEX, în schimb, nu are un format binar, ci unul text: exact aceiași octeți sunt împărțiți în 4 biți și sunt prezentați caracter cu caracter în format ASCII și sunt folosite doar caracterele 0-9, A-F (bin și hex sunt sisteme numerice cu baze multiple, adică 4 biți pe bin pot fi reprezentați ca o singură cifră hexadecimală). Deci formatul ihex este de peste 2 ori dimensiunea celui obișnuit fisier binar(fiecare 4 biți sunt înlocuiți cu un octet + întreruperi de linie pentru o citire ușoară), dar poate fi citit într-un editor de text obișnuit. Prin urmare, dacă urmează să trimiteți acest fișier cuiva, sau să îl utilizați în alte programe de programare, atunci este indicat să îl redenumiți stm32_md_vl.bin pentru a nu induce în eroare cei care se uită la numele lui.

Așa că am configurat versiunea de firmware pentru stm32. Data viitoare o să vă spun cum

Când tocmai începi să programezi microcontrolere sau nu ai făcut programare de mult timp, nu este ușor să înțelegi codul altcuiva. Întrebări „Ce este asta?” și „De unde a venit asta?” apar pe aproape fiecare combinație de litere și numere. Și cu cât înțelegerea logicii „ce? de ce? și unde?” vine mai rapid, cu atât este mai ușor să studiezi codul altor oameni, inclusiv exemple. Adevărat, uneori, pentru aceasta, trebuie să „săriți prin cod” și „căutați prin manuale” mai mult de o zi.

Toate microcontrolerele STM32F4xx au destul de multe periferice. Fiecărui dispozitiv periferic microcontroler îi este atribuită o zonă de memorie specifică, specifică și nerelocabilă. Fiecare zonă de memorie constă din registre de memorie, iar acești registre pot fi de 8 biți, 16 biți, 32 de biți sau altceva, în funcție de microcontroler. În microcontrolerul STM32F4, aceste registre sunt pe 32 de biți și fiecare registru are propriul său scop și propria sa adresă specifică. Nimic nu te împiedică să le accesezi direct în programele tale, indicând adresa. La ce adresă se află acest sau acel registru și ce dispozitiv periferic aparține este indicat pe cardul de memorie. Pentru STM32F4 un astfel de card de memorie se află în documentul DM00031020.pdf, care poate fi găsit pe st.com. Documentul este numit

RM0090
Manual de referință
STM32F405xx/07xx, STM32F415xx/17xx, STM32F42xxx și STM32F43xxx MCU avansate pe 32 de biți bazate pe ARM

În capitolul 2.3 Harta memoriei la pagina 64 un tabel începe cu adresele zonelor de registru și afilierea acestora cu dispozitivul periferic. În același tabel există un link către o secțiune cu alocarea mai detaliată a memoriei pentru fiecare periferic.

Tabelul din stânga arată gama de adrese, în mijloc este numele perifericului, iar în ultima coloană este situată o descriere mai detaliată a alocării memoriei.

Deci pentru porturile I/O scop general GPIO în tabelul de alocare a memoriei puteți găsi că adresele le sunt alocate începând de la 0x4002 0000. Portul I/O de uz general GPIOA ocupă intervalul de adrese de la 0x4002 000 la 0x4002 03FF. Portul GPIOB ocupă domeniul de adrese 0x4002 400 - 0x4002 07FF. Și așa mai departe.

Pentru a vedea o distribuție mai detaliată în gama în sine, trebuie doar să urmați linkul.

Există și un tabel aici, dar cu o hartă de memorie pentru intervalul de adrese GPIO. Conform acestei hărți de memorie, primii 4 octeți aparțin registrului MODER, următorii 4 octeți aparțin registrului OTYPER și așa mai departe. Adresele de registru sunt numărate de la începutul intervalului aparținând unui anumit port GPIO. Adică, fiecare registru GPIO are o adresă specifică care poate fi utilizată la dezvoltarea programelor pentru microcontroler.

Dar utilizarea adreselor de registru este incomod pentru oameni și este plină de un număr mare de erori. Prin urmare, producătorii de microcontrolere creează biblioteci standard care facilitează lucrul cu microcontrolere. În aceste biblioteci, adresele fizice sunt asociate cu desemnarea literei lor. Pentru STM32F4xx aceste corespondențe sunt specificate în fișier stm32f4xx.h. Fişier stm32f4xx.h aparține bibliotecii CMSISși se află în folderul Libraries\CMSIS\ST\STM32F4xx\Include\.

Să vedem cum este definit portul GPIOA în biblioteci. Orice altceva este determinat în mod similar. Este suficient să înțelegem principiul. Fişier stm32f4xx.h destul de mare și, prin urmare, mai bine să utilizați căutarea sau capacitățile oferite de lanțul dvs. de instrumente.

Pentru portul GPIOA, găsim linia care menționează GPIOA_BASE

GPIOA_BASE este definit prin AHB1PERIPH_BASE

AHB1PERIPH_BASE este, la rândul său, definit prin PERIPH_BASE

Și, la rândul său, PERIPH_BASE este definit ca 0x4000 0000. Dacă vă uitați la harta de distribuție a memoriei dispozitivelor periferice (în secțiunea 2.3 Harta memoriei la pagina 64), vom vedea această adresă chiar în partea de jos a tabelului. De la această adresă încep registrele tuturor perifericelor microcontrolerului STM32F4. Adică, PERIPH_BASE este adresa de pornire a întregii periferii a microcontrolerelor STM32F4xx în general și a microcontrolerului STM32F407VG în special.

AHB1PERIPH_BASE este definit ca suma (PERIPH_BASE + 0x00020000). (vezi pozele din spate). Aceasta va fi adresa 0x4002 0000. În cardul de memorie, porturile de intrare/ieșire GPIO de uz general încep la această adresă.

GPIOA_BASE este definit ca (AHB1PERIPH_BASE + 0x0000), adică este adresa de pornire a grupului de registre de porturi GPIOA.

Ei bine, portul GPIOA în sine este definit ca o structură de registre, a căror plasare în memorie începe cu adresa GPIOA_BASE (vezi linia #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE).

Structura fiecărui port GPIO este definită ca GPIO_TypeDef.

Astfel, bibliotecile standard, în acest caz fișierul stm32f4xx.h, pur și simplu umanizați adresarea mașinii. Dacă vedeți intrarea GPIOA->ODR = 1234, atunci aceasta înseamnă că numărul 1234 va fi scris la adresa 0x40020014. GPIOA are o adresă de pornire de 0x40020000 și registrul ODR are o adresă de 0x14 de la începutul intervalului, deci GPIOA->ODR are o adresă de 0x40020014.

Sau, de exemplu, nu vă place intrarea GPIOA->ODR, atunci puteți defini #define GPIOA_ODR ((uint32_t *) 0x40020014)și obțineți același rezultat scriind GPIOA_ODR = 1234;. Dar cât de oportun este acest lucru? Dacă doriți cu adevărat să vă introduceți propriile denumiri, atunci este mai bine să le reatribuiți pur și simplu pe cele standard. Puteți vedea cum se face acest lucru în fișier stm32f4_discovery.h De exemplu, așa este definit unul dintre LED-uri acolo:

#define LED4_PIN GPIO_Pin_12
#define LED4_GPIO_PORT GPIOD
#define LED4_GPIO_CLK RCC_AHB1Periph_GPIOD

Mai mult descriere detaliata periferia porturilor este situată în stm32f4xx_gpio.h

O listă de articole care vor ajuta chiar și un începător să învețe microcontrolerul STM32. Detalii despre orice, cu exemple, de la LED-uri intermitente la controlul motorului fără perii. Exemplele folosesc standardul SPL (Standard Peripheral Library).

Placă de testare STM32F103, programator ST-Link și software pentru firmware pentru Windows și Ubuntu.

VIC (controler de întrerupere cu vector imbricat) – modul de control al întreruperii. Configurarea și utilizarea întreruperilor. Întreruperea priorităților. întreruperi imbricate.

ADC ( convertor analog-digital). Diagrama de alimentare și exemple de utilizare a ADC în diferite moduri. Canale obișnuite și injectate. Folosind ADC cu DMA. Termometru intern. Câine de pază analogic.

Temporizatoare de uz general. Generarea unei întreruperi la intervale regulate. Măsurarea timpului dintre două evenimente.

Captarea semnalului cu un temporizator folosind exemplul de lucru cu un senzor ultrasonic HC-SR04

Utilizarea unui temporizator pentru a lucra cu un encoder.

Generație PWM. Controlul luminozității LED. Controlul servo-acționării (servo). Generare de sunet.