Come configurare smartphone e PC. Portale informativo
  • casa
  • notizia
  • STM32F407(STM32F4-DISCOVERY) - Approccio non standard - Libreria standard parte 1.

STM32F407(STM32F4-DISCOVERY) - Approccio non standard - Libreria standard parte 1.

Software necessario per lo sviluppo. In questo articolo ti dirò come configurarlo e collegarlo correttamente. Tutti gli ambienti commerciali come IAR EWARM o Keil uVision di solito fanno questa integrazione da soli, ma nel nostro caso tutto dovrà essere configurato manualmente, spendendo molto tempo su di esso. Il vantaggio è che hai la possibilità di capire come funziona tutto dall'interno e di personalizzare ulteriormente tutto in modo flessibile. Prima di iniziare il setup, considera la struttura dell'ambiente in cui lavoreremo:

Eclipse verrà utilizzato per modificare facilmente i file di implementazione delle funzioni ( .C), file di intestazione ( .h), così come i file di assieme ( .S). Per "conveniente" intendo l'uso del completamento del codice, l'evidenziazione della sintassi, il refactoring, la navigazione tra le funzioni ei loro prototipi. I file vengono inviati automaticamente ai compilatori appropriati che generano il codice oggetto (in file .o). Finora, questo codice non contiene indirizzi assoluti di variabili e funzioni e quindi non è adatto per l'esecuzione. I file oggetto risultanti vengono messi insieme dal linker. Per sapere quali parti dello spazio degli indirizzi utilizzare, il raccoglitore utilizza un file speciale ( .ld), chiamato script di collegamento. Di solito contiene le definizioni degli indirizzi delle sezioni e le loro dimensioni (sezione di codice mappata su flash, sezione variabile mappata su RAM, ecc.).

Alla fine, il linker genera un file .elf (Executable and Linkable Format), che contiene, oltre alle istruzioni e ai dati, le informazioni di Debugging utilizzate dal debugger. Per il normale flashing con vsprog, questo formato non è adatto, poiché richiede un file immagine di memoria più primitivo (ad esempio, Intel HEX - .hex). Per generarlo, c'è anche uno strumento del set Sourcery CodeBench (arm-none-eabi-objcopy) e si integra perfettamente in Eclipse usando il plugin ARM installato.

Per implementare il debug stesso, vengono utilizzati tre programmi:

  1. eclipse stesso, che consente al programmatore di utilizzare "visivamente" il debug, camminare lungo le linee, spostare il cursore del mouse sulle variabili per visualizzarne i valori e altre comodità
  2. arm-none-eabi-gdb - client GDB - debugger, che è nascosto controllato da eclips (tramite stdin) come reazione alle azioni specificate nel paragrafo 1. A sua volta, GDB si connette al server OpenOCD Debug e tutti i comandi di input vengono tradotti dal debugger GDB in comandi comprensibili per OpenOCD. canale GDB<->OpenOCD è implementato sul protocollo TCP.
  3. OpenOCD è un server di debug in grado di comunicare direttamente con il programmatore. Funziona davanti al client e attende una connessione TCP.

Questo schema può sembrarti molto inutile: perché usare il client e il server separatamente ed eseguire nuovamente la traduzione dei comandi, se tutto questo può essere fatto con un debugger? La questione è che tale architettura teoricamente permette di fare lo scambio del client e del server convenientemente. Ad esempio, se è necessario utilizzare un altro programmatore invece di versaloon, che non supporterà OpenOCD, ma supporterà un altro server di debug speciale (ad esempio, texane / stlink per il programmatore stlink - che si trova nella scheda di debug STM32VLDiscovery), allora invece di eseguire OpenOCD, avvierai semplicemente il server desiderato e tutto dovrebbe funzionare, senza gesti aggiuntivi. Allo stesso tempo, è possibile la situazione inversa: supponiamo che tu voglia utilizzare l'ambiente IAR EWARM invece del bundle Eclipse + CodeBench, insieme a versaloon. IAR ha il proprio client di debug integrato che comunicherà correttamente con OpenOCD e lo guiderà, oltre a ricevere i dati necessari in risposta. Tuttavia, tutto questo a volte rimane solo in teoria, poiché gli standard per la comunicazione tra client e server non sono strettamente regolamentati, e in alcuni punti possono differire, tuttavia, le configurazioni che ho indicato con st-link + eclipse e IAR + versaloon hanno avuto successo per me.

In genere, il client e il server vengono eseguiti sulla stessa macchina e la connessione al server avviene su host locale:3333(Per openocd), o host locale:4242(per texane/stlink st-util). Ma nessuno si preoccupa di aprire la porta 3333 o 4242 (e inoltrare questa porta sul router alla rete esterna) ei tuoi colleghi di un'altra città saranno in grado di connettersi ed eseguire il debug del tuo pezzo di ferro. Questo trucco viene spesso utilizzato dagli embedder che lavorano su siti remoti con accesso limitato.

Iniziare

Esegui eclipse e seleziona File->Nuovo->Progetto C, seleziona il tipo di progetto ARM Linux GCC (Sorcery G++ Lite) e il nome "stm32_ld_vl" (se hai STV32VLDiscovery, sarebbe più logico chiamarlo "stm32_md_vl") :

Fare clic su Fine, ridurre a icona o chiudere la finestra di benvenuto. Quindi, il progetto viene creato e la cartella stm32_ld_vl dovrebbe apparire nel tuo spazio di lavoro. Ora deve essere riempito con le librerie necessarie.

Come hai capito dal nome del progetto, creerò un progetto per la vista del righello linea di valore a bassa densità(LD_VL). Per creare un progetto per altri microcontrollori, devi sostituire tutti i file e le definizioni a nome del quale c'è _LD_VL (o_ld_vl) a quelli che ti servono, secondo la tabella:

Vista del righello Designazione Microcontrollori (x può cambiare)
Linea di valore a bassa densità _LD_VL STM32F100x4 STM32F100x6
bassa densità _LD STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Linea di valore a media densità _MD_VL STM32F100x8 STM32F100xB
media densità
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
Linea di valore ad alta densità _HD_VL STM32F100xC STM32F100xD STM32F100xE
alta densità _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
Densità XL _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Linea di connettività _CL STM32F105xx e STM32F107xx

Per comprendere la logica della tabella, è necessario conoscere le marcature STM32. Cioè, se hai VLDiscovery, dovrai inoltre sostituire tutto ciò che riguarda _LD_VL con _MD_VL, poiché il chip STM32F100RB relativo alla linea del valore di media densità è saldato in discovery.

Aggiunta della libreria di periferiche standard CMSIS e STM32F10x al progetto

CMSIS(Cortex Microcontroller Software Interface Standard) - una libreria standardizzata per lavorare con i microcontrollori Cortex che implementa il livello HAL (Hardware Abstraction Layer), ovvero consente di astrarre dai dettagli di lavorare con i registri, cercare indirizzi di registro utilizzando fogli dati, eccetera. La libreria è un insieme di codici sorgente in C e Asm. La parte principale (Core) della libreria è la stessa per tutti i Cortex (che si tratti di ST, NXP, ATMEL, TI o chiunque altro) ed è sviluppata da ARM. L'altra parte della libreria è responsabile delle periferiche, che naturalmente variano da produttore a produttore. Pertanto, alla fine, l'intera libreria è ancora distribuita dal produttore, sebbene la parte del kernel possa ancora essere scaricata separatamente dal sito Web ARM. La libreria contiene le definizioni degli indirizzi, il codice di inizializzazione del generatore di clock (opportunamente configurabile con defines), e tutto ciò che evita al programmatore di introdurre manualmente nei propri progetti la definizione degli indirizzi di tutti i registri periferici e la definizione dei bit dei valori di questi registri.

Ma i ragazzi della ST sono andati oltre. Oltre al supporto CMSIS forniscono un'altra libreria per STM32F10x chiamata Libreria periferica standard(SPL) che può essere utilizzato in aggiunta a CMSIS. La libreria fornisce un accesso più rapido e conveniente alle periferiche e controlla anche (in alcuni casi) il corretto funzionamento delle periferiche. Pertanto, questa libreria è spesso chiamata un insieme di driver per moduli periferici. È accompagnato da un pacchetto di esempi suddivisi in categorie per diverse periferiche. La libreria è disponibile non solo per STM32F10x, ma anche per altre serie.

È possibile scaricare l'intera versione 3.5 di SPL+CMSIS qui: STM32F10x_StdPeriph_Lib_V3.5.0 o sul sito Web della ST. Decomprimi l'archivio. Crea le cartelle CMSIS e SPL nella cartella del progetto e inizia a copiare i file nel tuo progetto:

Cosa copiare

Dove copiare (considerando
che la cartella del progetto sia stm32_ld_vl)

descrizione del file
Biblioteche/CMSIS/CM3/
Supporto principale/ core_cm3.c
stm32_ld_vl/CMSIS/ core_cm3.c Descrizione del nucleo Cortex M3
Biblioteche/CMSIS/CM3/
Supporto principale/ core_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h Intestazioni della descrizione del kernel

ST/STM32F10x/ system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c funzioni di inizializzazione e
controllo dell'orologio
Librerie/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h Intestazioni per queste funzioni
Librerie/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/ stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h Descrizione di base delle periferiche
Librerie/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/avvio/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! Attenzione estensione file MAIUSCOLA S)
File di tabella vettoriale
interrupt e init-ami su asm
Progetto/STM32F10x_StdPeriph_Modello/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/ stm32f10x_conf.h Modello per la personalizzazione
moduli periferici

inc/ *
stm32_ld_vl/SPL/inc/ * File di intestazione SPL
Librerie/STM32F10x_StdPeriph_Driver/
fonte/ *
stm32_ld_vl/SPL/src/ * Implementazione SPL

Dopo la copia, vai su Eclipse e fai Aggiorna nel menu contestuale del progetto. Di conseguenza, in Project Explorer dovresti ottenere la stessa struttura dell'immagine a destra.

Potresti aver notato che ci sono cartelle per IDE diversi nella cartella Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ (in IDE diversi vengono utilizzati compilatori diversi). Ho scelto l'IDE Ride7 perché utilizza il compilatore GNU Tools per ARM Embedded, che è compatibile con il nostro Sourcery CodeBench.

L'intera libreria viene configurata tramite il preprocessore (usando defines), questo permetterà di risolvere tutti i rami necessari in fase di compilazione (o meglio, anche prima) ed evitare il carico sul controller stesso (che si osserverebbe se il la configurazione è stata eseguita in RunTime). Ad esempio, tutta l'attrezzatura è diversa per i diversi righelli e quindi, affinché la libreria "sappia" quale righello si desidera utilizzare, viene chiesto di decommentare nel file stm32f10x.h una delle definizioni (corrispondente alla tua linea):

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

Eccetera...

Ma non consiglio di farlo. Per ora non toccheremo i file di libreria, ma definiremo define in seguito utilizzando le impostazioni del compilatore in Eclipse. E poi Eslipse chiamerà il compilatore con la chiave -D STM32F10X_LD_VL, che per il preprocessore è assolutamente equivalente alla situazione se non hai commentato "#define STM32F10X_LD_VL". Pertanto, non cambieremo il codice, di conseguenza, se lo desideri, un giorno sarai in grado di spostare la libreria in una directory separata e non copiarla nella cartella di ogni nuovo progetto.

Script di collegamento

Nel menu contestuale del progetto, selezionare Nuovo->File->Altro->Generale->File, Avanti. Seleziona la cartella principale del progetto (stm32_ld_vl). Immettere il nome del file "stm32f100c4.ld" (o "stm32f100rb.ld" per il rilevamento). Ora copia e incolla in Eclipse:

ENTRY (Reset_Handler) MEMORIA ( FLASH (rx): ORIGIN = 0x08000000, LENGTH = 16K RAM (xrw): ORIGIN = 0x20000000, LENGTH = 4K ) _estack = ORIGIN(RAM) + LENGTH(RAM); MIN_HEAP_SIZE = 0; MIN_STACK_SIZE = 256; SEZIONI ( /* Interrompe la tabella dei vettori */ .isr_vector: ( . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); ) >FLASH /* Il codice del programma e altri dati vanno in FLASH * / .text: ( . = ALIGN(4); /* Codice */ *(.text) *(.text*) /* Costanti */ *(.rodata) *(.rodata*) /* ARM->Thumb e Thumb->ARM codice colla */ *(.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. attributi: ( *(.ARM.attributes) ) > FLASH .preinit_array: ( PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); ) >FLASH .init_array: ( PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); ) >FLASH .fini_array: ( PROVIDE_HIDDEN (__fini_array_start = .); KEEP (* (.fini_array*)) KEEP (*(ORDINA(.fini_array.*))) FORNIRE_NASCOSTO (__fini_array_end = .); ) >FLASH _sidata = .; /* Dati inizializzati */ .data: AT (_sidata) ( . = ALIGN(4); _sdata = .; /* crea un simbolo globale all'inizio dei dati */ *(.data) *(.data*) . = ALIGN (4); _edata = .; /* definisce un simbolo globale alla fine dei dati */ ) >RAM /* Dati non inizializzati */ . = ALLINEA(4); .bss: ( /* Viene utilizzato dall'avvio per inizializzare la sezione .bss */ _sbss = .; /* definisce un simbolo globale all'inizio di bss */ __bss_start__ = _sbss; *(.bss) *(.bss *) *(COMMON) .= ALIGN(4); _ebss = .; /* definisce un simbolo globale alla fine di bss */ __bss_end__ = _ebss; ) >RAM PROVIDE(end = _ebss); FORNIRE(_fine = _ebss); FORNIRE(__HEAP_START = _ebss); /* Sezione User_heap_stack, utilizzata per verificare che sia rimasta RAM sufficiente */ ._user_heap_stack: ( . = ALIGN(4); . = . + MIN_HEAP_SIZE; . = . + MIN_STACK_SIZE; . = ALIGN(4); ) >RAM / SCARTA/ : ( libc.a(*) libm.a(*) libgcc.a(*) ) )

Questo l lo script inker sarà progettato specificatamente per il controller STM32F100C4 (che ha 16 KB di flash e 4 KB di RAM), se ne possiedi uno diverso dovrai modificare i parametri LENGTH delle aree FLASH e RAM all'inizio del il file (per STM32F100RB, che è in Discovery: Flash 128K e RAM 8K).

Salviamo il file.

Configurazione build (Build C/C++)

Vai su Progetto->Proprietà->C/C++ Build->Impostazioni->Impostazioni strumento e inizia a configurare gli strumenti di compilazione:

1) Precessore di destinazione

Scegliamo sotto quale core Cortex funzionerà il compilatore.

  • Processore: cortex-m3

2) Compilatore C ARM Sourcery Linux GCC -> Preprocessore

Aggiungiamo due define-a passandoli attraverso l'opzione -D al compilatore.

  • STM32F10X_LD_VL - definisce il righello (ne ho scritto sopra)
  • USE_STDPERIPH_DRIVER: indica alla libreria CMSIS che deve utilizzare il driver SPL

3) Compilatore ARM Sourcery Linux GCC C -> Directory

Aggiungi percorsi per includere le librerie.

  • "$(località_spazio di lavoro:/$(NomeProgetto)/CMSIS)"
  • "$(workspace_loc:/$(ProjName)/SPL/inc)"

Ora, per esempio, se scriviamo:

#include "stm32f10x.h

Quindi il compilatore deve prima cercare il file stm32f10x.h nella directory del progetto (lo fa sempre), non lo troverà lì e inizierà a cercare nella cartella CMSIS, il percorso a cui abbiamo indicato, beh, lo troverà.

4) Compilatore ARM Sourcery Linux GCC C -> Ottimizzazione

Abilita funzionalità e ottimizzazione dei dati

  • -sezioni di funzione
  • -fdata-sezioni

Di conseguenza, tutte le funzioni e gli elementi di dati verranno inseriti in sezioni separate e il raccoglitore sarà in grado di capire quali sezioni non vengono utilizzate e semplicemente buttarle via.

5) Compilatore ARM Sourcery Linux GCC C -> Generale

Aggiungi il percorso al nostro script del linker: "$(workspace_loc:/$(ProjName)/stm32f100c4.ld)" (o come lo chiami).

E imposta le opzioni:

  • Non utilizzare file di avvio standard - non utilizzare file di avvio standard.
  • Rimuovi le sezioni non utilizzate - rimuovi le sezioni non utilizzate

Questo è tutto, l'installazione è completa. OK.

Dalla creazione del progetto, abbiamo fatto molte cose e qualcosa che Eclipse potrebbe non notare, quindi dobbiamo dirgli di rivedere la struttura dei file di progetto. Per fare ciò, dal menu contestuale del progetto, devi farlo Indice -> ricostruisci.

Ciao LED su STM32

È ora di creare il file di progetto principale: File -> Nuovo -> C/C++ -> File sorgente. prossimo. Nome file File sorgente: main.c.

Copia e incolla quanto segue nel file:

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // Abilita PORTB Periph clock RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Abilita TIM2 Periph clock // Disabilita JTAG per rilascio LED PIN RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // Cancella bit del registro di controllo PB4 e PB5 GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 | GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // Configura PB.4 e PB.5 come uscita Push Pull al massimo 10Mhz GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec TIM2->ARR = 1000; // 1 Interrupt/1 sec TIM2->DIER |= TIM_DIER_UIE; // Abilita interrupt tim2 TIM2->CR1 |= TIM_CR1_CEN; // Avvia conteggio NVIC_EnableIRQ(TIM2_IRQn); // Abilita IRQ while(1); // Loop infinito ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UIF ; //Pulisci flag UIF if (1 == (i++ & 0x1)) ( GPIOB->BSRR = GPIO_BSRR_BS4; // Imposta bit PB4 GPIOB->BSRR = GPIO_BSRR_BR5; // Reimposta bit PB5 ) else ( GPIOB->BSRR = GPIO_BSRR_B S5; // Imposta PB5 bit GPIOB->BSRR = GPIO_BSRR_BR4; // Ripristina bit PB4 ) )

Sebbene abbiamo incluso le librerie SPL, non è stato utilizzato qui. Tutti gli accessi al campo come RCC->APB2ENR sono completamente documentati in CMSIS.

Puoi eseguire Project -> Build All. Se tutto ha funzionato, il file stm32_ld_vl.hex dovrebbe apparire nella cartella Debug del progetto. È stato generato automaticamente da elf dagli strumenti integrati. Facciamo lampeggiare il file e vediamo come i LED lampeggiano con una frequenza di una volta al secondo:

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

Naturalmente, invece di /home/user/workspace/ devi inserire il tuo percorso verso lo spazio di lavoro.

Per STM32VLDiscovery

Il codice è leggermente diverso da quello che ho fornito sopra per il mio scialle di debug. La differenza sta nei pin su cui "si appendono" i LED. Se nella mia scheda erano PB4 e PB5, in Discovery sono PC8 e PC9.

#include "stm32f10x.h" uint8_t i=0; int main(void) ( RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Abilita PORTC Periph clock RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Abilita TIM2 Periph clock // Cancella i bit del registro di controllo PC8 e PC9 GPIOC->CRH &= ~ (GPIO_CRH_MODE8 | GPIO_CRH_CNF8 | GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // Configura PC8 e PC9 come uscita Push Pull a max 10 Mhz GPIOC->CRH |= GPIO_CRH_MODE8_0 | GPIO_CRH_MODE9_0; TIM2->PSC = SystemCoreClock / 1000 - 1; // 10000 TIM2-> ARR = 1000; // 1 Interrupt/sec (1000/100) TIM2->DIER |= TIM_DIER_UIE; // Abilita interrupt tim2 TIM2->CR1 |= TIM_CR1_CEN; // Avvia conteggio NVIC_EnableIRQ(TIM2_IRQn); // Abilita IRQ mentre (1); // Ciclo infinito ) void TIM2_IRQHandler(void) ( TIM2->SR &= ~TIM_SR_UIF; //Pulisci flag UIF if (1 == (i++ & 0x1)) ( GPIOC->BSRR = GPIO_BSRR_BS8 ; // Imposta PC8 bit GPIOC->BSRR = GPIO_BSRR_BR9; // Ripristina PC9 bit ) else (GPIOC->BSRR = GPIO_BSRR_BS9; // Imposta PC9 bit GPIOC->BSRR = GPIO_BSRR_BR8; // Ripristina PC8 bit ) )

In Windows, puoi eseguire il flashing dell'esadecimale risultante (/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) con un'utilità di ST.

Bene, sotto Linux, l'utilità st-flash. MA!!! L'utilità non utilizza il formato esadecimale del formato Intel HEX (che viene generato per impostazione predefinita), quindi è estremamente importante selezionare il formato binario nelle impostazioni per la creazione di un'immagine Flash:

L'estensione del file non cambierà (hex rimarrà com'era), ma il formato del file cambierà. E solo dopo puoi fare:

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

A proposito, a scapito dell'estensione e del formato: di solito i file binari sono contrassegnati con l'estensione .bin, mentre i file in formato Intel HEX sono chiamati estensione .hex. La differenza in questi due formati è più tecnica che funzionale: il formato binario contiene semplicemente byte di istruzioni e dati che verranno semplicemente scritti nel controller dal programmatore "così com'è". IntelHEX, invece, non ha un formato binario, ma testuale: esattamente gli stessi byte sono divisi in 4 bit e sono presentati carattere per carattere in formato ASCII, e vengono utilizzati solo i caratteri 0-9, AF ( bin e hex sono sistemi numerici con basi multiple, ovvero 4 bit in bin possono essere rappresentati come una singola cifra in esadecimale). Quindi il formato ihex è più di 2 volte la dimensione di un normale file binario (ogni 4 bit vengono sostituiti da un byte + interruzioni di riga per una facile lettura), ma può essere letto in un normale editor di testo. Pertanto, se hai intenzione di inviare questo file a qualcuno, o usarlo in altri programmi di programmazione, è consigliabile rinominarlo in stm32_md_vl.bin, in modo da non fuorviare coloro che guarderanno il suo nome.

Quindi abbiamo impostato l'assembly del firmware per stm32. La prossima volta ti dico come

Quando si crea la prima applicazione sul microcontrollore STM32, ci sono diversi modi per procedere. Per prima cosa, quella classica, prendiamo la descrizione esatta del controllore sul sito www.st.com, che compare sotto la denominazione "Manuale di riferimento" e leggiamo la descrizione dei registri periferici. Quindi proviamo a scriverli e vediamo come funzionano le periferiche. Leggere questo documento è molto utile, ma nella prima fase della padronanza del microcontrollore, questo può essere abbandonato, stranamente. Gli ingegneri di STMicroelectronics hanno scritto una libreria di driver per periferiche standard. Inoltre, hanno scritto molti esempi di utilizzo di questi driver, che possono ridurre la programmazione dell'applicazione alla pressione dei tasti Ctrl + C e Ctrl + V, seguiti da una piccola modifica nell'esempio del driver per soddisfare le tue esigenze. Pertanto, la connessione della libreria del driver periferico al progetto è il secondo metodo per creare un'applicazione. Oltre alla velocità di scrittura, ci sono altri vantaggi di questo metodo: l'universalità del codice e l'uso di altre librerie proprietarie, come USB, Ethernet, drive control, ecc., che sono fornite nei sorgenti e utilizzano un driver periferico standard. Ci sono anche degli svantaggi di questo metodo: dove puoi cavartela con una riga di codice, il driver della periferica STM32 standard scriverà 10. La libreria della periferica stessa è fornita anche come file sorgente, quindi puoi tracciare quale bit di quale registro cambia questo o quella funzione. Volendo sarà possibile passare dal secondo metodo di scrittura di un programma al primo commentando una parte del codice che utilizza da sola la libreria standard, che controlla direttamente il registro periferico. Come risultato di tale azione, guadagnerai velocità di controllo, quantità di RAM e ROM e perderai l'universalità del codice. In ogni caso, gli ingegneri Promelectronica consigliano di utilizzare la libreria delle periferiche standard almeno nella prima fase.

Le maggiori difficoltà attendono lo sviluppatore quando si collega la libreria al suo progetto. Se non sai come farlo, puoi dedicare molto tempo a questo evento, che contraddice l'idea stessa di utilizzare un driver già pronto. Il materiale è dedicato al collegamento della libreria standard a qualsiasi famiglia STM32.

Ciascuna famiglia STM32 dispone della propria libreria di periferiche standard. Ciò è dovuto al fatto che la periferia stessa è diversa. Ad esempio, le periferiche dei controller STM32L hanno una funzione di risparmio energetico come uno dei compiti, che comporta l'aggiunta di funzioni di controllo. Un classico esempio è l'ADC, che in STM32L ha la capacità di spegnere l'hardware, in assenza di un comando di conversione per molto tempo - una delle conseguenze del compito di risparmio energetico. I controller ADC delle famiglie STM32F non hanno tale funzione. Infatti, per la presenza di una differenza hardware in periferia, abbiamo diverse librerie di driver. Oltre all'ovvia differenza nelle funzioni del controller, c'è un miglioramento nelle periferiche. Quindi, le periferiche dei controllori delle famiglie che sono state rilasciate in seguito possono essere più ponderate e convenienti. Ad esempio, le periferiche dei controller STM32F1 e STM32F2 presentano differenze di controllo. A parere dell'autore, la gestione delle periferiche STM32F2 è più conveniente. E questo è comprensibile perché: la famiglia STM32F2 è stata rilasciata successivamente e questo ha permesso agli sviluppatori di tenere conto di alcune sfumature. Di conseguenza, per queste famiglie - biblioteche di controllo periferiche individuali. L'idea di quanto sopra è semplice: nella pagina del microcontrollore che andrete ad utilizzare, c'è una libreria periferica adatta ad esso.

Nonostante la differenza di periferiche nelle famiglie, i conducenti nascondono dentro di sé il 90% delle differenze. Ad esempio, la funzione di ottimizzazione dell'ADC sopra menzionata è la stessa per tutte le famiglie:

void ADC_Init(ADC_Nom, ADC_Param),

dove ADC_Nom è il numero ADC sotto forma di ADC1, ADC2, ADC3, ecc.

ADC_Param - puntatore alla struttura dati, come configurare l'ADC (da cosa partire, quanti canali digitalizzare, se farlo ciclicamente, ecc.)

Il 10% delle differenze familiari, in questo esempio, che dovranno essere corrette quando si passa da una famiglia STM32 all'altra, sono nascoste nella struttura ADC_Param. A seconda della famiglia, il numero di campi in questa struttura può essere diverso. La parte generale ha la stessa sintassi. Pertanto, il trasferimento di un'applicazione per una famiglia STM32, scritta sulla base di librerie periferiche standard, ad un'altra è molto semplice. In termini di universalizzazione di soluzioni basate su microcontrollori, STMicroelectronics è irresistibile!

Quindi, abbiamo scaricato la libreria per l'STM32 utilizzato. Qual è il prossimo? Successivamente, dobbiamo creare un progetto e collegare ad esso i file richiesti. Prendi in considerazione la creazione di un progetto utilizzando l'ambiente di sviluppo IAR Embedded Workbench come esempio. Avviamo l'ambiente di sviluppo e andiamo nella scheda "Progetto", selezioniamo la voce "Crea progetto" per creare il progetto:

Nel nuovo progetto che compare, entra nelle impostazioni passando con il mouse sopra il nome del progetto, premendo il tasto destro del mouse e selezionando "Opzioni" dal menu a tendina:

Aree di memoria di RAM e ROM:

Quando si fa clic sul pulsante "Salva", l'ambiente proporrà di scrivere un nuovo file di descrizione del controller nella cartella del progetto. L'autore consiglia di creare un singolo file *.icp per ogni progetto e di salvarlo nella cartella del progetto.

Se hai intenzione di eseguire il debug del tuo progetto in-circuit, cosa consigliata, inserisci il tipo di debugger da utilizzare:

Nella scheda del debugger selezionato, specificare l'interfaccia per collegare il debugger (nel nostro caso, ST-Link è selezionato) al controller:



D'ora in poi, il nostro progetto senza librerie è pronto per essere compilato e caricato nel controllore. Altri ambienti come Keil uVision4, Resonance Ride7, ecc. dovranno seguire gli stessi passaggi.

Se scrivi la riga nel file main.c:

#include "stm32f10x.h" o

#include "stm32f2xx.h" o

#include "stm32f4xx.h" o

#include "stm32l15x.h" o

#include "stm32l10x.h" o

#include "stm32f05x.h"

indicando la posizione di questo file, o copiando questo file nella cartella del progetto, alcune aree di memoria verranno associate ai registri periferici della famiglia corrispondente. Il file stesso si trova nella cartella della libreria periferica standard nella sezione: \CMSIS\CM3\DeviceSupport\ST\STM32F10x (o simile nel nome per altre famiglie). D'ora in poi, sostituisci l'indirizzo del registro periferico con un numero con il suo nome. Anche se non si utilizzano le funzioni della libreria standard, si consiglia di effettuare tale connessione.

Se intendi utilizzare gli interrupt nel tuo progetto, ti consigliamo di includere un file di avvio con estensione *.s, che si trova lungo il percorso \CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\iar, o simile, per altre famiglie. È importante notare che ogni ambiente ha il proprio file. Di conseguenza, se utilizziamo IAR EWB, dobbiamo prendere il file dalla cartella IAR. Ciò è dovuto a una leggera differenza nella sintassi degli ambienti. Pertanto, affinché il progetto possa iniziare immediatamente, gli ingegneri di STMicroelectronics hanno scritto diverse varianti di file di avvio per molti degli ambienti di sviluppo più diffusi. La maggior parte delle famiglie STM32 ha un file. La famiglia STM32F1 dispone di diversi file di avvio:

  • startup_stm32f10x_cl.s - per microcontrollori STM32F105/107
  • startup_stm32f10x_xl.s - per microcontrollori STM32F101/STM32F103 768kb o più
  • startup_stm32f10x_hd.s - per microcontrollori STM32F101/STM32F103 con memoria Flash 256-512 kb
  • startup_stm32f10x_md.s - per microcontrollori STM32F101/STM32F102/STM32F103 con memoria Flash 64-128 kb
  • startup_stm32f10x_ld.s - per microcontrollori STM32F101/STM32F102/STM32F103 con memoria Flash inferiore a 64kb
  • startup_stm32f10x_hd_vl.s per microcontrollori STM32F100 con memoria Flash 256-512 kb
  • startup_stm32f10x_md_vl.s per microcontrollori STM32F100 con memoria Flash 64-128 kb
  • startup_stm32f10x_ld_vl.s per microcontrollori STM32F100 con memoria Flash 32kb o inferiore

Quindi, a seconda della famiglia, della sottofamiglia e dell'ambiente di sviluppo, aggiungi il file di avvio al progetto:

È qui che finisce il microcontrollore all'avvio del programma. L'interrupt chiama in sequenza la funzione SystemInit() e quindi __iar_program_start. La seconda funzione reimposta o scrive i valori predefiniti delle variabili globali, dopodiché passa al programma utente main(). La funzione SystemInit() imposta l'orologio del microcontrollore. È lei che risponde alle domande:

  • Devo passare al cristallo esterno (HSE)?
  • Come moltiplicare la frequenza da HSI/HSE?
  • È necessario connettere la coda di download dei comandi?
  • Quale ritardo è necessario durante il caricamento di un comando (a causa della bassa velocità della memoria Flash)
  • Come suddividere la timbratura dei bus periferici?
  • Il codice deve essere inserito nella RAM esterna?

La funzione SystemInit() può essere scritta manualmente nel progetto. Se emetti questa funzione come vuota, il controller funzionerà su un generatore RC interno con una frequenza di circa 8 MHz (a seconda del tipo di famiglia). Opzione 2 - collegare il file system_stm32f10x.c al progetto (o simile nel nome a seconda del tipo di famiglia utilizzata), che si trova nella libreria lungo il percorso: Librerie\CMSIS\CM3\DeviceSupport\ST\STM32F10x. Questo file contiene la funzione SystemInit(). Prestare attenzione alla frequenza del cristallo HSE_VALUE esterno. Questo parametro è impostato nel file di intestazione stm32f10x.h. Il valore standard è 8 e 25 MHz, a seconda della famiglia STM32. Il compito principale della funzione SystemInit() è commutare l'orologio su un quarzo esterno e moltiplicare questa frequenza in un certo modo. Cosa succede se il valore di HSE_VALUE è 8MHz, il core dovrebbe avere un clock a 72MHz e infatti la scheda ha un quarzo a 16MHz? Come risultato di tali azioni errate, il core riceverà un clock di 144 MHz, che potrebbe essere al di là del funzionamento garantito del sistema su STM32. Quelli. quando si include il file system_stm32f10x.c, sarà necessario specificare il valore HSE_VALUE. Tutto ciò significa che i file system_stm32f10x.c, system_stm32f10x.h e stm32f10x.h (o simili nel nome per altre famiglie) devono essere individuali per ogni progetto. E

Gli ingegneri di STMicroelectronics hanno creato lo strumento di configurazione dell'orologio, che consente di configurare correttamente l'orologio di sistema. Questo è un file Excel che genera un file system_stm32xxx.c (simile nel nome a una determinata famiglia di famiglie) dopo aver impostato i parametri di input e output del sistema. Considera il suo lavoro sull'esempio della famiglia STM32F4.

Opzioni: oscillatore RC interno, oscillatore RC interno con moltiplicazione di frequenza o cristallo esterno con moltiplicazione di frequenza. Dopo aver selezionato la sorgente di clock, inseriamo i parametri della configurazione di sistema desiderata, come la frequenza di ingresso (quando si utilizza quarzo esterno), la frequenza di core clock, i divisori di frequenza di clock dei bus periferici, il funzionamento del buffer di recupero delle istruzioni, e altri. Facendo clic sul pulsante "Genera", otteniamo una finestra


Il collegamento del file system_stm32f4xx.c e dei suoi analoghi richiederà il collegamento di un altro file della libreria standard della periferica. Per controllare l'orologio, c'è un intero set di funzioni che vengono chiamate dal file system_stm32xxxxxx.c. Queste funzioni si trovano nel file stm32f10x_rcc.c e nella relativa intestazione. Di conseguenza, quando si collega il file system_stm32xxxxxx.c al progetto, è necessario collegare stm32f10x_rcc.c, altrimenti il ​​linker dell'ambiente segnalerà l'assenza di una descrizione della funzione con il nome RCC_xxxxxxx. Il file specificato si trova nella libreria periferica lungo il percorso: Libraries\STM32F10x_StdPeriph_Driver\src e la relativa intestazione \Libraries\STM32F10x_StdPeriph_Driver\inc.

I file di intestazione del driver periferico sono collegati nel file stm32f10x_conf.h, a cui fa riferimento stm32f10x.h. Il file stm32f10x_conf.h è semplicemente un insieme di file di intestazione del driver per specifiche periferiche del controller da includere nel progetto. Inizialmente, tutte le intestazioni "#include" sono contrassegnate come commenti. Il collegamento del file di intestazione della periferia consiste nel rimuovere il commento dal nome del file corrispondente. Nel nostro caso, questa è la riga #include "stm32f10x_rcc.h". Ovviamente, il file stm32f10x_conf.h è individuale per ogni progetto, perché progetti diversi utilizzano periferiche diverse.

E l'ultimo. È necessario specificare diverse direttive per il preprocessore del compilatore e percorsi per i file di intestazione.



I percorsi dei file header possono essere diversi, a seconda della posizione della libreria periferica relativa alla cartella del progetto, ma la presenza di “USE_STDPERIPH_DRIVER” è obbligatoria quando si collegano i driver delle periferiche della libreria standard.

Quindi, abbiamo collegato la libreria standard al progetto. Inoltre, abbiamo collegato uno dei driver periferici standard al progetto che controlla l'orologio di sistema.

Abbiamo imparato come appare il dispositivo della libreria dall'interno, ora alcune parole su come appare dall'esterno.



Pertanto, l'inclusione del file di intestazione stm32f10x.h in un'applicazione implica l'inclusione di altri file di intestazione e file di codice. Alcuni di quelli mostrati nella figura sono descritti sopra. Qualche parola sul resto. I file STM32F10x_PPP.x sono file di driver periferici. Un esempio di connessione di un tale file è mostrato sopra, questo è RCC - la periferia del controllo dell'orologio di sistema. Se vogliamo connettere i driver di altre periferiche, allora il nome dei file collegati si ottiene sostituendo “PPP” con il nome della periferica, ad esempio ADC - STM32F10x_ADC.s, o porte I/O STM32F10x_GPIO.s, oppure DAC - STM32F10x_DAC.s. In generale, è intuitivamente chiaro quale file deve essere collegato quando si collega una determinata periferica. I file "misc.c", "misc.h" sono generalmente lo stesso STM32F10x_PPP.x, controllano solo il kernel. Ad esempio, impostare i vettori di interrupt, che sono integrati nel kernel, o gestire il timer SysTick, che fa parte del kernel. I file xxxxxxx_it.c descrivono i vettori NMI del controller. Possono essere integrati con vettori di interrupt periferici. Il file core_m3.h descrive il core CortexM3. Questo core è standardizzato e può essere trovato in microcontrollori di altri produttori. Per l'universalizzazione multipiattaforma, STMicroelectronics ha lavorato per creare una libreria core CortexM separata, dopodiché ARM l'ha standardizzata e distribuita ad altri produttori di microcontrollori. Quindi il passaggio a STM32 dai controller di altri produttori con un core CortexM sarà un po' più semplice.

Quindi, possiamo collegare la libreria di periferiche standard a qualsiasi famiglia STM32. Chi ha imparato a farlo aspetta un premio: una semplicissima programmazione di microcontrollori. La libreria, oltre ai driver sotto forma di file sorgente, contiene molti esempi di utilizzo delle periferiche. Ad esempio, considera la creazione di un progetto che coinvolga le uscite di confronto del timer. Con l'approccio tradizionale, studieremo attentamente la descrizione dei registri di questa periferica. Ma ora possiamo studiare il testo del programma in corso. Entriamo nella cartella degli esempi di periferiche standard, che si trova lungo il percorso ProjectSTM32F10x_StdPeriph_Examples. Di seguito sono riportate cartelle di esempi con il nome delle periferiche utilizzate. Andiamo nella cartella "TIM". I timer in STM32 hanno molte funzioni e impostazioni, quindi è impossibile dimostrare le capacità del controller con un esempio. Pertanto, all'interno della directory specificata, ci sono molti esempi di utilizzo dei timer. Siamo interessati alla generazione di un segnale PWM da un timer. Andiamo nella cartella "7PWM_Output". All'interno c'è una descrizione del programma in inglese e una serie di file:

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

Se il progetto non ha interruzioni, il contenuto si trova interamente nel file main.c. Copia questi file nella directory del progetto. Dopo aver compilato il progetto, otterremo un programma per l'STM32, che configurerà il timer e le porte I/O per generare 7 segnali PWM dal timer 1. Successivamente, possiamo adattare il codice già scritto al nostro compito. Ad esempio, ridurre il numero di segnali PWM, modificare il duty cycle, la direzione di conteggio, ecc. Le funzioni ei relativi parametri sono ben descritti nel file stm32f10x_stdperiph_lib_um.chm. I nomi delle funzioni ei loro parametri sono facilmente associabili al loro scopo per chi conosce un po' di inglese. Per chiarezza, ecco una parte del codice dell'esempio preso:

/* Configurazione della base dei tempi */ TIM_TimeBaseStructure.TIM_Prescaler = 0; // nessun contatore impulsi prescaler (registro a 16 bit) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // verso l'alto TIM_TimeBaseStructure TIM_Period = TimerPeriod; // contare fino al valore di TimerPeriod (costante nel programma) TIM_TimeBaseStructure.TIM_ClockDivision = 0; // nessuna predivisione dei contatori TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // contatore di overflow per la generazione di eventi (non utilizzato nel programma) TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // immissione dei valori TimeBaseStructure nei registri del timer 1 (l'immissione dei dati in questa // variabile è superiore) /* Canale 1, 2, 3 e 4 Configurazione in modalità PWM */ // configurazione delle uscite PWM TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2 ; // Modalità operativa PWM2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // abilita l'uscita dei segnali temporizzati PWM TIM_OCInitStructure.TIM_OutputNSate = TIM_OutputNSate_Enable; // abilita l'uscita timer PWM complementare TIM_OCInitStructure.TIM_Pulse = Channel1Pulse; // larghezza dell'impulso Channel1Pulse è una costante nel programma TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; // impostazione della polarità di uscita TIM_OCInitStructure TIM_OCNPolarità = TIM_OCNPolarità_High; // impostazione della polarità dell'uscita complementare TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; // imposta lo stato di uscita PWM sicuro TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset; // imposta lo stato sicuro dell'uscita PWM complementare TIM_OC1Init(TIM1, &TIM_OCInitStructure); // inserire i valori della variabile TIM_OCInitStructure nei registri PWM del canale 1 // di timer1 TIM_OCInitStructure.TIM_Pulse = Channel2Pulse; // modifica l'ampiezza dell'impulso nella variabile OCInitStructure e inseriscila in TIM_OC2Init(TIM1, &TIM_OCInitStructure); // timer1 canale 2 registri PWM TIM_OCInitStructure.TIM_Pulse = Channel3Pulse; // modifica l'ampiezza dell'impulso nella variabile OCInitStructure e inseriscila in TIM_OC3Init(TIM1, &TIM_OCInitStructure); // timer1 canale 3 registri PWM TIM_OCInitStructure.TIM_Pulse = Channel4Pulse; // modifica l'ampiezza dell'impulso nella variabile OCInitStructure e inseriscila in TIM_OC4Init(TIM1, &TIM_OCInitStructure); // timer1 canale 4 registri PWM /* abilitazione contatore TIM1 */ TIM_Cmd(TIM1, ENABLE); // avvia timer1 /* Abilita uscita principale TIM1 */ TIM_CtrlPWMOutputs(TIM1, ENABLE); // abilita il timer 1 per confrontare le uscite

Sul lato destro, l'autore ha lasciato un commento in russo per ogni riga del programma. Se apriamo lo stesso esempio nella descrizione delle funzioni delle librerie stm32f10x_stdperiph_lib_um.chm, vedremo che tutti i parametri di funzione utilizzati hanno un link alla propria descrizione, dove verranno indicati i loro possibili valori. Le funzioni stesse hanno anche un collegamento alla propria descrizione e codice sorgente. Questo è molto utile perché sapendo cosa fa la funzione, possiamo tracciare come lo fa, quali bit dei registri periferici e come influisce. Questa è, in primo luogo, un'altra fonte di informazioni per la padronanza del controller, basata sull'uso pratico del controller. Quelli. prima risolvi il problema tecnico, quindi studi la soluzione stessa. In secondo luogo, questo è un campo per l'ottimizzazione del programma per coloro che non sono soddisfatti della libreria in termini di velocità e dimensione del codice.



Bene, finora tutto sta andando bene, ma sono pronte solo lampadine e pulsanti. Ora è il momento di affrontare le periferiche più pesanti: USB, UART, I2C e SPI. Ho deciso di iniziare con USB: il debugger ST-Link (anche quello reale di Discovery) si è ostinatamente rifiutato di eseguire il debug della mia scheda, quindi il debug sulle stampe tramite USB è l'unico metodo di debug a mia disposizione. Puoi, ovviamente, tramite UART, ma questo è un mucchio di cavi aggiuntivi.

Di nuovo ho fatto molta strada: ho generato gli spazi vuoti corrispondenti in STM32CubeMX, aggiunto il middleware USB dal pacchetto STM32F1Cube al mio progetto. Devi solo abilitare il clock USB, definire i gestori di interrupt USB appropriati e rifinire le piccole cose. Per la maggior parte, ho copiato tutte le impostazioni importanti del modulo USB da STM32GENERIC, tranne per il fatto che ho archiviato leggermente l'allocazione della memoria (hanno usato malloc e io ho usato l'allocazione statica).

Ecco un paio di pezzi interessanti che mi sono trascinato dentro. Ad esempio, affinché l'host (computer) capisca che qualcosa è stato collegato ad esso, il dispositivo "si destreggia" con la linea USB D + (che è collegata al pin A12). Vedendo questo, l'host inizia a interrogare il dispositivo per sapere chi è, quali interfacce può, a quale velocità vuole comunicare, ecc. Non capisco davvero perché questo debba essere fatto prima dell'inizializzazione USB, ma in stm32duino è fatto più o meno allo stesso modo.

Tremolio USB

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate() ( // Inizializza pin PA12 GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed ​​​​= GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &pinInit); // Comunica all'host di enumerare i dispositivi USB su il bus 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 altro punto interessante è il supporto per il bootloader stm32duino. Per caricare il firmware, devi prima riavviare il controller nel bootloader. Il modo più semplice è premere il pulsante di ripristino. Ma per renderlo più conveniente, puoi imparare dall'esperienza Arduino. Quando gli alberi erano giovani, i controller AVR non avevano ancora il supporto USB a bordo, c'era un adattatore USB-UART sulla scheda. Il segnale DTR dell'UART è collegato al reset del microcontrollore. Quando l'host invia un segnale DTR, il microcontrollore viene ricaricato nel bootloader. Funziona con il cemento!

Nel caso di utilizzo di USB, emuliamo solo la porta COM. Di conseguenza, devi riavviare tu stesso il bootloader. Il caricatore stm32duino, oltre al segnale DTR, prevede anche una speciale costante magica per ogni evenienza (1EAF - un riferimento a Leaf Labs)

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t length) ( ... case CDC_SET_CONTROL_LINE_STATE: dtr_pin++; //il pin DTR è abilitato break; ... static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len) ( /* Quattro byte è il pacchetto magico "1EAF" che inserisce l'MCU nel bootloader.*/ if(*Len >= 4) ( /** * Controlla se l'ingresso contiene la stringa "1EAF".* Se sì, controlla se il DTR ha */ if(dtr_pin > 3) ( if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&& (Buf == "F")) ( HAL_NVIC_SystemReset( ); ) dtr_pin = 0; ) ) ... )

Indietro: Mini Arduino

In generale, USB guadagnato. Ma questo livello funziona solo con i byte, non con le stringhe. Ecco perché le stampe di debug sembrano così brutte.

CDC_Transmit_FS((uint8_t*)"Ping\n", 5); // 5 è uno strlen ("Ping") + zero byte
Quelli. non esiste alcun supporto per l'output formattato: né si stampa un numero né si assembla una stringa da pezzi. Emergono le seguenti opzioni:

  • Al diavolo il classico printf. L'opzione sembra non essere male, ma attira + 12kb di firmware (in qualche modo ho chiamato accidentalmente sprintf in me stesso)
  • Trova la tua implementazione di printf nella tua scorta. Una volta ho scritto sotto AVR, come se questa implementazione fosse più piccola.
  • Avvita la classe Print di arduino nell'implementazione STM32GENERIC
Ho scelto quest'ultima opzione perché anche il codice della libreria Adafruit GFX si basa su Print, quindi devo ancora rovinarlo. Inoltre, avevo già il codice STM32GENERIC a portata di mano.

Ho creato una directory MiniArduino nel mio progetto per inserire la quantità minima di codice necessaria per implementare i pezzi dell'interfaccia arduino di cui ho bisogno. Ho iniziato a copiare un file alla volta e vedere quali altre dipendenze sono necessarie. Questo mi ha dato una copia della classe Print e alcuni file di wrapping.

Ma questo non basta. Come prima, era necessario collegare in qualche modo la classe Print con le funzioni USB (ad esempio, CDC_Transmit_FS()). Per fare ciò, ho dovuto inserire la classe SerialUSB. Ha portato con sé la classe Stream e un pezzo di inizializzazione GPIO. Il passo successivo è stato collegare l'UART (ho il GPS collegato ad esso). Quindi ho anche inserito la classe SerialUART, che ha estratto un altro livello di inizializzazione delle periferiche da STM32GENERIC.

In generale, mi sono trovato nella seguente situazione. Ho copiato quasi tutti i file da STM32GENERIC al mio MiniArduino. Avevo anche la mia copia delle librerie USB e FreeRTOS (avrei dovuto avere copie di HAL e CMSIS, ma ero troppo pigro). Allo stesso tempo, ho segnato il tempo per un mese e mezzo, collegando e scollegando diversi pezzi, ma allo stesso tempo non ho scritto una sola riga di nuovo codice.

È diventato chiaro che la mia idea originale di assumere il controllo dell'intera parte del sistema non ha avuto molto successo. Ad ogni modo, parte del codice di inizializzazione risiede in STM32GENERIC e sembra essere più comodo per lui lì. Ovviamente, è stato possibile tagliare tutte le dipendenze e scrivere le tue classi wrapper per le tue attività, ma questo mi rallenterebbe per un altro mese: questo codice deve ancora essere sottoposto a debug. Certo, per il tuo CSV sarebbe bello, ma devi andare avanti!

In generale, ho eliminato tutte le librerie duplicate e quasi l'intero livello di sistema e sono tornato a STM32GENERIC. Questo progetto si sta sviluppando in modo abbastanza dinamico: diversi commit al giorno sono stabili. Inoltre, nell'ultimo mese e mezzo, ho studiato molto, letto la maggior parte del Manuale di riferimento STM32, guardato come sono realizzate le librerie HAL e i wrapper STM32GENERIC, avanzato nella comprensione dei descrittori USB e delle periferiche del microcontrollore. In generale, ora ero molto più sicuro dell'STM32GENERIC rispetto a prima.

Indietro: I2C

Tuttavia, le mie avventure non sono finite qui. C'erano ancora UART e I2C (ho un display lì). Con UART, tutto era abbastanza semplice. Ho appena rimosso l'allocazione dinamica della memoria e in modo che gli UART inutilizzati non mangino questa stessa memoria, li ho semplicemente commentati.

Ma l'implementazione di I2C in STM32GENERIC ha piantato un kaku. Ciò che è molto interessante, ma che mi ha portato almeno 2 sere. Bene, o ho dato 2 serate di duro debug sulle stampe: questo è da che parte guardare.

In generale, l'implementazione del display non è iniziata. Nello stile già tradizionale, non funziona e basta. Cosa non funziona non è chiaro. La libreria del display stesso (Adafruit SSD1306) sembra essere stata testata sulla precedente implementazione, ma non sono da escludere interferenze di bug. I sospetti cadono su HAL e sull'implementazione I2C di STM32GENERIC.

Per cominciare, ho commentato tutto il display e il codice I2C e ho scritto l'inizializzazione I2C senza alcuna libreria, in puro HAL

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


Ho scaricato lo stato dei registri subito dopo l'inizializzazione. Ho fatto lo stesso dump nella versione funzionante su stm32duino. Ecco cosa ho ottenuto (con commenti a me stesso)

Buono (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: Interrupt di errore abilitato, 36 Mhz
40005408: 0 0 0 0 - I2C_OAR1: zero proprio indirizzo

40005410: 0 0 0 AF - I2C_DR: registro dati

40005418: 0 0 0 0 - I2C_SR2: registro di stato

Cattivo (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: Abilitazione periferica
40005404: 0 0 0 24 - I2C_CR2: 36 Mhz
40005408: 0 0 40 0 ​​​​- I2C_OAR1: !!! Bit non descritto nel registro indirizzi impostato
4000540C: 0 0 0 0 - I2C_OAR2: proprio registro indirizzi
40005410: 0 0 0 0 - I2C_DR: registro dati
40005414: 0 0 0 0 - I2C_SR1: registro di stato
40005418: 0 0 0 2 - I2C_SR2: bit occupato impostato
4000541C: 0 0 80 1E - I2C_CCR: modalità 400kHz
40005420: 0 0 0 B - I2C_TRISE

La prima grande differenza è il 14° bit impostato nel registro I2C_OAR1. Questo bit non è affatto descritto nella scheda tecnica e rientra nella sezione riservata. Vero, a condizione che uno debba ancora essere scritto lì. Quelli. questo è un bug in libmaple. Ma poiché tutto funziona lì, il problema non è in questo. Scaviamo ulteriormente.

Un'altra differenza è il bit occupato impostato. All'inizio non gli attribuivo nessuna importanza, ma guardando avanti, dirò che è stato lui a segnalare il problema!.. Ma prima di tutto.

Ho pasticciato il codice di inizializzazione sul ginocchio senza alcuna libreria.

Visualizza l'inizializzazione

void sendCommand(I2C_HandleTypeDef * handle, uint8_t cmd) ( SerialUSB.print("Invio comando "); SerialUSB.println(cmd, 16); uint8_t xBuffer; xBuffer = 0x00; xBuffer = cmd; HAL_I2C_Master_Transmit(handle, I2C1_DEVICE_ADDRESS<<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); }


Dopo qualche sforzo, questo codice ha funzionato per me (in questo caso ho disegnato delle strisce). Quindi il problema è nel livello I2C di STM32GENERIC. Ho iniziato a rimuovere gradualmente il mio codice, sostituendolo con le parti appropriate dalla libreria. Ma non appena ho cambiato il codice di inizializzazione del pin dalla mia implementazione a quella della libreria, l'intero trasferimento I2C ha iniziato a scadere in timeout.

Poi mi sono ricordato della parte occupata e ho cercato di capire quando si verifica. Si è scoperto che il flag di occupato si verifica non appena il codice di inizializzazione attiva l'orologio I2c. Quelli. Il modulo si accende e subito non funziona. Interessante.

Caduta durante l'inizializzazione

uint8_t * pv = (uint8_t*)0x40005418; //Registro I2C_SR2. Alla ricerca di flag BUSY SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // Stampa 0 __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // stampa 2


Sopra questo codice c'è solo l'inizializzazione dei pin. Bene, cosa fare: copriamo il debug con le stampe attraverso la linea e lì

Inizializzazione del pin STM32GENERIC

void stm32AfInit(const stm32_af_pin_list_type list, int size, const void *instance, GPIO_TypeDef *port, uint32_t pin, uint32_t mode, uint32_t pull) ( … GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = pin; GPIO_InitStruct.Mode = modalità; GPIO_InitStruct = modalità; GPIO_InitStruct ; GPIO_InitStruct.Mode = modalità; ;GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_VERY_HIGH;HAL_GPIO_Init(porta, &GPIO_InitStruct); ... )


Ma ecco il problema: GPIO_InitStruct è compilato correttamente. Solo il mio funziona, questo no. Davvero mistico!!! Tutto è come da manuale, ma niente funziona. Ho studiato riga per riga il codice della biblioteca alla ricerca di qualcosa di sospetto. Alla fine mi sono imbattuto in questo codice (chiama la funzione sopra)

Un altro pezzo di inizializzazione

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


Vedi un bug in esso? E lei è! Ho anche rimosso i parametri non necessari per rendere il problema più visibile. In generale, la differenza è che il mio codice inizializza entrambi i pin contemporaneamente in una struttura e il codice STM32GENERIC a sua volta. Apparentemente il codice di inizializzazione del pin influisce in qualche modo sul livello su questo pin. Prima dell'inizializzazione, non viene emesso nulla su questo pin e il livello viene portato all'unità dal resistore. Al momento dell'inizializzazione, per qualche ragione, il controller imposta zero sulla gamba corrispondente.

Questo fatto di per sé è innocuo. Ma il problema è che l'abbassamento della linea SDA mentre la linea SCL è attiva è una condizione di partenza per il bus i2c. Per questo motivo, il ricevitore del controller impazzisce, imposta il flag BUSY e inizia ad attendere i dati. Ho deciso di non sventrare la libreria per aggiungere la possibilità di inizializzare più pin contemporaneamente. Invece, ho appena scambiato queste 2 righe: l'inizializzazione del display è riuscita. La correzione è stata adottata in STM32GENERIC.

A proposito, l'inizializzazione del bus in libmaple è interessante. Prima di iniziare l'inizializzazione delle periferiche i2c sul bus, viene prima effettuato un reset. Per fare ciò, la libreria mette i pin nella consueta modalità GPIO e fa oscillare più volte queste gambe, simulando le sequenze di avvio e arresto. Questo aiuta a dare vita ai dispositivi bloccati sul bus. Sfortunatamente, non esiste una cosa simile in HAL. A volte il mio display si blocca e quindi spegne solo il risparmio energetico.

inizializzazione i2c da stm32duino

/** * @brief Reimposta un bus I2C. * * Il ripristino viene eseguito eseguendo il clock out degli impulsi fino a quando eventuali slave bloccati * rilasciano SDA e SCL, quindi generano una condizione di START, quindi una condizione di STOP *. * * @param dev I2C device */ void i2c_bus_reset(const i2c_dev *dev) ( /* Rilascia entrambe le linee */ i2c_master_release_bus(dev); /* * Assicurati che il bus sia libero sincronizzandolo fino a quando gli slave non rilasciano il * bus. */ while (!gpio_read_bit(sda_port(dev), dev->sda_pin)) ( /* Attendi la fine di qualsiasi allungamento del clock */ while (!gpio_read_bit(scl_port(dev), dev->scl_pin)) ; delay_us(10 ); /* Tira in basso */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); /* Rilascia di nuovo in alto */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); ) /* Genera una condizione di avvio e poi di arresto */ 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); )

Di nuovo lì: UART

Ero entusiasta di tornare finalmente alla programmazione e continuare a scrivere funzionalità. Il prossimo grande pezzo è stato il collegamento della scheda SD tramite SPI. Questo di per sé è eccitante, interessante e pieno di dolore. Ne parlerò sicuramente separatamente nel prossimo articolo. Uno dei problemi era un carico elevato (> 50%) del processore. Ciò ha messo in discussione l'efficienza energetica del dispositivo. Sì, e l'utilizzo del dispositivo era scomodo, perché. UI stupido terribilmente.

Comprendendo il problema, ho trovato il motivo di tale consumo di risorse. Tutto il lavoro con la scheda SD è stato svolto byte per byte, tramite il processore. Se fosse necessario scrivere un blocco di dati sulla scheda, allora per ogni byte viene chiamata la funzione di invio di un byte

Per (uint16_t i = 0; i< 512; i++) { spiSend(src[i]);
No, beh, non è grave! C'è anche DMA! Sì, la libreria SD (quella fornita con Arduino) è goffa e deve essere cambiata, ma il problema è più globale. La stessa immagine si osserva nella libreria dello schermo e anche l'ascolto dell'UART è stato effettuato tramite un sondaggio. In generale, ho iniziato a pensare che riscrivere tutti i componenti in HAL non fosse un'idea così stupida.

Ho iniziato, ovviamente, con qualcosa di più semplice: il driver UART, che ascolta il flusso di dati dal GPS. L'interfaccia Arduino non ti consente di collegarti a un interrupt UART e di strappare i caratteri in arrivo al volo. Di conseguenza, l'unico modo per ottenere dati è un sondaggio costante. Ovviamente, ho aggiunto vTaskDelay(10) al gestore GPS per ridurre un po' il carico, ma questa è in realtà una stampella.

Il primo pensiero, ovviamente, è stato quello di rovinare la DMA. Funzionerebbe anche se non fosse per il protocollo NMEA. Il problema è che in questo protocollo le informazioni vengono semplicemente trasmesse in streaming e i singoli pacchetti (linee) sono separati da un carattere di interruzione di riga. Inoltre, ogni linea può essere di diverse lunghezze. Per questo motivo, non è noto in anticipo quanti dati devono essere ricevuti. DMA non funziona così - lì il numero di byte deve essere impostato in anticipo durante l'inizializzazione del trasferimento. Insomma, il DMA scompare, si cerca un'altra soluzione.

Se guardi da vicino il design della libreria NeoGPS, puoi vedere che la libreria accetta dati di input byte per byte, ma i valori vengono aggiornati solo quando è arrivata l'intera linea (per essere più precisi, un pacchetto di più righe). Quella. non importa se si alimentano i byte della libreria uno alla volta man mano che vengono ricevuti, o poi tutti in una volta. Quindi, puoi risparmiare tempo del processore: salva la linea ricevuta nel buffer e puoi farlo direttamente nell'interrupt. Quando la stringa viene ricevuta nella sua interezza, l'elaborazione può iniziare.

Emerge il seguente disegno

Classe di guida UART

// Dimensione del buffer di input UART const uint8_t gpsBufferSize = 128; // Questa classe gestisce l'interfaccia UART che riceve i caratteri dal GPS e li memorizza in una classe buffer GPS_UART ( // UART hardware handle UART_HandleTypeDef uartHandle; // Receive ring buffer uint8_t rxBuffer; volatile uint8_t lastReadIndex = 0; volatile uint8_t lastReceivedIndex = 0; / /Gps thread handle TaskHandle_t xGPSThread = NULL;


Sebbene l'inizializzazione sia leccata da STM32GENERIC, è pienamente coerente con quella offerta da CubeMX

Inizializzazione UART

void init() ( // Reimposta i puntatori (nel caso qualcuno chiami init() più volte) lastReadIndex = 0; lastReceivedIndex = 0; // Inizializza l'handle del thread GPS xGPSThread = xTaskGetCurrentTaskHandle(); // Abilita il clock del periperhal corrispondente __HAL_RCC_GPIOA_CLK_ENABLE( ); __HAL_RCC_USART1_CLK_ENABLE (); // Init perni in modalità funzioni secondaria GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; // TX pin GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init (GPIOA, e 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 e GPIO_InitStruct); // Init uartHandle.Instance = USART1; uartHandle.Init.BaudRate = 9600; uartHandle WordLength = UART_WORDLENGTH_8B; uartHandle.Init.StopBits = UART_STOPBITS_1; uartHandle.Init.Parity = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.H wFlowCtl = UART_HWCONTROL_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // Useremo l'interrupt UART per ottenere i dati HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Aspetteremo un singolo carattere ricevuto direttamente nel buffer HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); )


In effetti, non è stato possibile inizializzare il pin TX, ma uartHandle.Init.Mode potrebbe essere impostato su UART_MODE_RX - lo accetteremo solo. Tuttavia, lascia che sia: improvvisamente ho bisogno di configurare in qualche modo il modulo GPS e scriverci i comandi.

Il design di questa classe avrebbe potuto essere migliore se non fosse stato per i limiti dell'architettura HAL. Quindi, non possiamo semplicemente impostare la modalità, dicono, accettare tutto, allegare direttamente all'interrupt e strappare i byte ricevuti direttamente dal registro ricevente. Dobbiamo dire in anticipo a HAL quanti e dove riceveremo byte: gli stessi gestori corrispondenti scriveranno i byte ricevuti nel buffer fornito. Per questo, nell'ultima riga della funzione di inizializzazione c'è una chiamata a HAL_UART_Receive_IT(). Poiché la lunghezza della stringa non è nota in anticipo, è necessario ricevere un byte.

È inoltre necessario dichiarare fino a 2 richiamate. Uno è un gestore di interrupt, ma il suo compito è semplicemente chiamare il gestore dall'HAL. La seconda funzione è il "callback" di HAL che il byte è già stato ricevuto ed è già nel buffer.

Richiamate UART

// Inoltra l'elaborazione dell'interrupt UART a HAL extern "C" void USART1_IRQHandler(void) ( HAL_UART_IRQHandler(gpsUart.getUartHandle()); ) // HAL chiama questo callback quando riceve un carattere da UART. Inoltralo alla classe extern "C" void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) ( gpsUart.charReceivedCB(); )


Il metodo charReceivedCB() prepara l'HAL a ricevere il byte successivo. Ed è lui che determina che la linea è già terminata e puoi segnalarlo al programma principale. Come mezzo di sincronizzazione, potrebbe essere utilizzato un semaforo in modalità segnale, ma per scopi così semplici si consiglia di utilizzare le notifiche dirette.

Elaborazione byte ricevuti

// Char ricevuto, prepara per il prossimo inline void charReceivedCB() ( char lastReceivedChar = rxBuffer; lastReceivedIndex++; HAL_UART_Receive_IT(&uartHandle, rxBuffer + (lastReceivedIndex % gpsBufferSize), 1); // Se viene ricevuto un simbolo EOL, notifica al thread GPS quella riga è disponibile per la lettura if(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); )


La funzione di risposta (in attesa) è waitForString(). Il suo compito è semplicemente appendere l'oggetto di sincronizzazione e attendere (o uscire con un timeout)

Aspettando la fine della linea

// Attendi fino alla ricezione dell'intera riga bool waitForString() ( return ulTaskNotifyTake(pdTRUE, 10); )


Funziona così. Il thread responsabile del GPS normalmente dorme nella funzione waitForString(). I byte provenienti dal GPS vengono aggiunti al buffer dal gestore degli interrupt. Se arriva il carattere \n (fine riga), l'interrupt riattiva il thread principale, che inizia a versare byte dal buffer nel parser. Bene, quando il parser termina l'elaborazione del batch di messaggi, aggiornerà i dati nel modello GPS.

Flusso GPS

void vGPSTask(void *pvParameters) ( // L'inizializzazione GPS deve essere eseguita all'interno del thread GPS poiché l'handle del thread è memorizzato // e utilizzato in seguito per scopi di sincronizzazione gpsUart.init(); for (;;) ( // Attendi fino a quando l'intera stringa è ricevuto if(!gpsUart.waitForString()) continue; // Legge la stringa ricevuta e analizza il flusso GPS char per char while(gpsUart.available()) ( int c = gpsUart.readChar(); //SerialUSB.write(c) ; gpsParser.handle(c); ) if(gpsParser.available()) ( GPSDataModel::instance().processNewGPSFix(gpsParser.read()); GPSDataModel::instance().processNewSatellitesData(gpsParser.satellites, gpsParser.sat_count ); ) vTaskDelay(10); ) )


Mi sono imbattuto in un momento molto non banale, che è rimasto bloccato per diversi giorni. Sembra che il codice di sincronizzazione sia stato preso dagli esempi, ma all'inizio non ha funzionato: ha bloccato l'intero sistema. Pensavo che il problema fosse nelle notifiche dirette (funzioni xTaskNotifyXXX), l'ho cambiato in normali semafori, ma l'applicazione ha comunque riattaccato.

Si è scoperto che devi stare molto attento con la priorità di interruzione. Per impostazione predefinita, ho impostato tutti gli interrupt a priorità zero (la più alta). Ma FreeRTOS richiede che le priorità rientrino in un determinato intervallo. Gli interrupt con una priorità troppo alta non possono chiamare le funzioni di FreeRTOS. Solo gli interrupt con una priorità di configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY e inferiori possono chiamare le funzioni di sistema (buona spiegazione e ). Per impostazione predefinita, questa impostazione è impostata su 5. Ho modificato la priorità di interrupt UART su 6 e tutto è stato avviato.

Di nuovo lì: I2C tramite DMA

Ora puoi fare qualcosa di più complicato, come un driver video. Ma qui devi fare un'escursione nella teoria del bus I2C. Di per sé, questo bus non regola il protocollo per la trasmissione dei dati sul bus: puoi scrivere byte o leggere. È anche possibile prima scrivere, quindi leggere in una transazione (ad esempio, annotare l'indirizzo e quindi leggere i dati a questo indirizzo).

Tuttavia, la maggior parte dei dispositivi definisce il protocollo di livello superiore più o meno allo stesso modo. il dispositivo fornisce all'utente una serie di registri, ciascuno con il proprio indirizzo. Allo stesso tempo, nel protocollo di comunicazione, il primo (o più) byte di ogni transazione determina l'indirizzo della cella (registro) in cui leggeremo o scriveremo ulteriormente. In questo caso è possibile anche uno scambio multibyte nello stile di “scriviamo / leggiamo molti byte a partire da questo indirizzo”. Quest'ultima opzione è adatta per DMA.

Sfortunatamente, il display basato sul controller SSD1306 fornisce un protocollo completamente diverso: quello di comando. Il primo byte di ogni transazione è il segno "comando o dati". Nel caso di un comando, il secondo byte è il codice del comando. Se il comando necessita di argomenti, vengono passati come comandi separati dopo il primo. Per inizializzare il display, è necessario inviare circa 30 comandi, ma non possono essere aggiunti in un array e inviati in un blocco. Devi inviarli uno per uno.

Ma con l'invio di un array di pixel (frame buffer), è del tutto possibile utilizzare i servizi di DMA. Questo è ciò che proveremo.

Ma la libreria Adafruit_SSD1306 è scritta in modo molto goffo ed è impossibile inserirla con poco sangue. Apparentemente, la libreria è stata inizialmente scritta per comunicare con il display tramite SPI. Quindi qualcuno ha aggiunto il supporto I2C e il supporto SPI è rimasto abilitato. Poi qualcuno ha iniziato ad aggiungere tutti i tipi di ottimizzazioni di basso livello e a nasconderle dietro ifdef "s. Di conseguenza, abbiamo ottenuto noodles dal codice per supportare varie interfacce. Quindi, prima di andare oltre, abbiamo dovuto rispolverare.

All'inizio ho provato a ripulirlo avvolgendo il codice per diverse interfacce con ifdefs. Ma se voglio scrivere codice per comunicare con il display, usare DMA e sincronizzare tramite FreeRTOS, allora non ci riuscirò. Più precisamente risulterà, ma questo codice dovrà essere scritto direttamente nel codice della libreria. Pertanto, ho deciso di pensare ancora una volta alla libreria, creare un'interfaccia e inserire ogni driver in una classe separata. Il codice è diventato più pulito e sarebbe possibile aggiungere in modo indolore il supporto per nuovi driver senza modificare la libreria stessa.

Visualizza l'interfaccia del driver

// Interfaccia per driver hardware // L'Adafruit_SSD1306 non funziona direttamente con l'hardware // Tutte le richieste di comunicazione vengono inoltrate alla classe driver ISSD1306Driver ( public: virtual void begin() = 0; virtual void sendCommand(uint8_t cmd) = 0 ; virtual void sendData(uint8_t * data, size_t size) = 0; );


Quindi andiamo. Ho già mostrato l'inizializzazione di I2C. Non è cambiato niente lì. E qui con l'invio di un comando è stato un po' semplificato. Ricordi che ho parlato della differenza tra registro e protocollo di comando per i dispositivi I2C? E sebbene il display implementi un protocollo di comando, può essere ben imitato utilizzando un protocollo di registro. Devi solo immaginare che il display abbia solo 2 registri: 0x00 per i comandi e 0x40 per i dati. E HAL fornisce anche una funzione per questo tipo di trasferimento

Invio di un comando al display

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


All'inizio, non era molto chiaro con l'invio dei dati. Il codice sorgente ha inviato i dati in piccoli pacchetti di 16 byte

Strano codice di invio

per (uint16_t i=0; i


Ho provato a giocare con le dimensioni dei pacchetti e a inviare pacchetti più grandi, ma nella migliore delle ipotesi ho ottenuto un display accartocciato. Bene, o tutto è sospeso.

display accartocciato



Il motivo si è rivelato banale: un buffer overflow. La classe Arduino Wire (almeno STM32GENERIC) fornisce un proprio buffer di soli 32 byte. Ma perché abbiamo bisogno di un buffer aggiuntivo se la classe Adafruit_SSD1306 ne ha già uno? Inoltre, con HAL, l'invio si ottiene in una riga

Corretto trasferimento dei dati

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


Quindi, metà del lavoro è fatto: abbiamo scritto un driver per il display su un HAL puro. Ma in questa versione richiede ancora risorse: 12% per un display 128x32 e 23% per un display 128x64. L'uso di DMA qui è già richiesto.

Innanzitutto, inizializziamo il DMA. Vogliamo implementare il trasferimento dei dati in I2C #1 e questa funzione risiede sul sesto canale DMA. Inizializziamo la copia byte per byte dalla memoria alle periferiche

Configurazione di DMA per I2C

// Abilitazione orologio controller DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Inizializza 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_NORMALE; hdma_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_tx); // Associa l'handle DMA inizializzato all'handle I2C __HAL_LINKDMA(&handle, hdmatx, hdma_tx); /* Init interrupt DMA */ /* Configurazione interrupt DMA1_Channel6_IRQn */ HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);


Gli interrupt sono una parte obbligatoria del progetto. In caso contrario, la funzione HAL_I2C_Mem_Write_DMA() avvierà una transazione I2C, ma nessuno la completerà. Ancora una volta, abbiamo a che fare con un design HAL ingombrante e la necessità di un massimo di due callback. Tutto è esattamente lo stesso di UART. Una funzione è il gestore degli interrupt: devi semplicemente reindirizzare la chiamata all'HAL. La seconda funzione è un segnale che i dati sono già stati inviati.

Gestori di interrupt DMA

"C" esterna void DMA1_Channel6_IRQHandler(void) ( HAL_DMA_IRQHandler(displayDriver.getDMAHandle()); ) "C" esterna void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c) ( displayDriver.transferCompletedCB(); )


Certo, non sottoporremo costantemente i sondaggi a I2C, ma il trasferimento è già terminato? Invece, è necessario dormire sull'oggetto di sincronizzazione e attendere il completamento del trasferimento

Trasferimento dati tramite DMA con sincronizzazione

void DisplayDriver::sendData(uint8_t * data, size_t size) ( // Avvia trasferimento dati HAL_I2C_Mem_Write_DMA(&handle, i2c_addr, 0x40, 1, data, size); // Attendi il completamento del trasferimento ulTaskNotifyTake(pdTRUE, 100); ) void DisplayDriver::transferCompletedCB() ( // Riprendi il thread di visualizzazione vTaskNotifyGiveFromISR(xDisplayThread, NULL); )


Il trasferimento dei dati richiede ancora 24 ms, praticamente un tempo di trasferimento puro di 1 kb (dimensione del buffer di visualizzazione) a 400 kHz. Solo in questo caso, la maggior parte delle volte, il processore dorme (o fa altre cose). L'utilizzo complessivo della CPU è sceso dal 23% a solo l'1,5-2%. Penso che valesse la pena lottare per questo indicatore!

Ancora una volta: SPI tramite DMA

Il collegamento di una scheda SD tramite SPI è stato in un certo senso più semplice: a questo punto avevo iniziato a rovinare la libreria sdfat e lì le persone gentili avevano già separato la comunicazione con la scheda in un'interfaccia del driver separata. È vero, con l'aiuto di define, puoi scegliere solo una delle 4 versioni già pronte del driver, ma questo potrebbe essere facilmente sprecato e sostituire la tua stessa implementazione.

Interfaccia del driver SPI per lavorare con la scheda SD

// Questa è un'implementazione personalizzata della classe SPI Driver. La libreria SdFat // sta usando questa classe per accedere alla scheda SD tramite SPI // // L'intenzione principale di questa implementazione è guidare il trasferimento dei dati // su DMA e sincronizzarsi con le capacità di FreeRTOS. class SdFatSPIDriver: public SdSpiBaseDriver ( // modulo SPI SPI_HandleTypeDef spiHandle; // GPS thread handle TaskHandle_t xSDThread = NULL; public: SdFatSPIDriver(); virtual voidactivate(); virtual void begin(uint8_t chipSelectPin); virtual void deactivate(); virtual uint8_t receiver(); virtual uint8_t receiver(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 deseleziona(); );


Come prima, iniziamo con uno semplice, con un'implementazione Oak senza alcun DMA. L'inizializzazione è in parte generata da CubeMX e in parte derivata dall'implementazione SPI di STM32GENERIC

Inizializzazione SPI

SdFatSPIDriver::SdFatSPIDriver() ( ) //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) ( // Ignora il pin CS passato - Questo driver funziona con un (void)chipSelectPin predefinito; // Inizializza l'handle del thread GPS xSDThread = xTaskGetCurrentTaskHandle(); // Abilita il clock del periperhal corrispondente __HAL_RCC_GPIOA_CLK_ENABLE() ; __HAL_RCC_SPI1_CLK_ENABLE (); // Init perni GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7; // MOSI & SCK GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed ​​= GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init (GPIOA, e GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_6; // MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init (GPIOA, e GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_4; // CS GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init (GPIOA e GPIO_InitStruct); // Imposta il pin CS alto per impostazione predefinita HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // Init SPI spiHandl e.Istanza = 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); )


Il design dell'interfaccia è su misura per arduino con la numerazione dei pin con un unico numero. Nel mio caso, non aveva senso impostare il pin CS attraverso i parametri: ho questo segnale strettamente legato al pin A4, ma dovevo seguire l'interfaccia.

In base alla progettazione della libreria SdFat, la velocità della porta SPI è configurata prima di ogni transazione. Quelli. in teoria, puoi iniziare a comunicare con la scheda a bassa velocità, quindi aumentarla. Ma ho rinunciato e ho impostato la velocità una volta nel metodo begin(). Quindi i metodi di attivazione / disattivazione si sono rivelati vuoti per me. Come setSpiSettings()

Gestori di transazioni banali

void SdFatSPIDriver::activate() ( // Non è necessaria alcuna attivazione speciale ) void SdFatSPIDriver::deactivate() ( // Non è necessaria alcuna disattivazione speciale ) void SdFatSPIDriver::setSpiSettings(const SPISettings & spiSettings) ( // Ignora le impostazioni - stiamo usando stesse impostazioni per tutti i trasferimenti)


I metodi di controllo del segnale CS sono piuttosto banali

Controllo del segnale 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_SET); )


Stiamo arrivando al più interessante: leggere e scrivere. La prima implementazione più quercia senza DMA

Trasferimento dati senza 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::receive(uint8_t* buf, size_t n) ( // TODO : Ricevi tramite DMA qui 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: Trasmetti su DMA qui HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n, 10); )


Nell'interfaccia SPI, i dati vengono ricevuti e trasmessi contemporaneamente. Per ricevere qualcosa, devi inviare qualcosa. Di solito HAL fa questo per noi: chiamiamo semplicemente la funzione HAL_SPI_Receive () e organizza sia l'invio che la ricezione. Ma in effetti, questa funzione invia la spazzatura che era nel buffer di ricezione.
Per vendere qualcosa di non necessario, devi prima acquistare qualcosa di non necessario (C) Prostokvashino

Ma c'è una sfumatura. Le schede SD sono molto capricciose. A loro non piace essere truffati a caso mentre la carta invia i dati. Pertanto, ho dovuto utilizzare la funzione HAL_SPI_TransmitReceive () e inviare forzatamente 0xffs durante la ricezione dei dati.

Prendiamo le misure. Lascia che un thread scriva 1kb di dati sulla scheda in un ciclo.

Codice di prova per l'invio del flusso di dati alla scheda SD

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


Con questo approccio riesce a scrivere circa 15-16kb al secondo. Non tanto. Ma si è scoperto che avevo già impostato il prescaler su 256. l'orologio SPI è impostato su un valore molto inferiore al possibile throughput. Sperimentalmente, ho scoperto che non ha senso impostare una frequenza superiore a 9 MHz (il prescaler è impostato su 8) - è impossibile ottenere una velocità di registrazione superiore a 100-110 kb / s (su un'altra unità flash, da tra l'altro, per qualche ragione, si potevano registrare solo 50-60 kb/s, e sul terzo generalmente solo 40 kb/s). Apparentemente tutto dipende dai timeout dell'unità flash stessa.

In linea di principio, questo è già più che sufficiente, ma pomperemo i dati tramite DMA. Stiamo operando nel solito modo. Innanzitutto l'inizializzazione. Abbiamo ricezione e trasmissione SPI rispettivamente sul secondo e terzo canale DMA.

Inizializzazione DMA

// Abilitazione orologio controller DMA __HAL_RCC_DMA1_CLK_ENABLE(); // Canale DMA Rx 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_NORMALE; dmaHandleRx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleRx); __HAL_LINKDMA(&spiHandle, hdmarx, dmaHandleRx); // Canale DMA Tx 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_NORMALE; dmaHandleTx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleTx); __HAL_LINKDMA(&spiHandle, hdmatx, dmaHandleTx);


Non dimenticare di abilitare gli interrupt. Per me andranno con priorità 8, leggermente inferiore a UART e I2C

Configurazione degli interrupt DMA

// L'impostazione DMA interrompe 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);


Ho deciso che il sovraccarico dell'esecuzione di DMA e della sincronizzazione per i trasferimenti brevi potrebbe superare il guadagno, quindi per i pacchetti piccoli (fino a 16 byte) ho lasciato la vecchia opzione. I pacchetti più lunghi di 16 byte vengono inviati tramite DMA. Il metodo di sincronizzazione è esattamente lo stesso della sezione precedente.

Trasferimento dati tramite DMA

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


Ovviamente nessuna interruzione. Tutto è come nel caso di I2C

Interruzioni DMA

SdFatSPIDriver esterno 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().hdmatx); ) extern "C" void HAL_SPI_TxCplt (SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); ) "C" esterna void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) ( spiDriver.dmaTransferCompletedCB(); )


Iniziamo, controlliamo. Per non tormentare l'unità flash, ho deciso di eseguire il debug sulla lettura di un file di grandi dimensioni e non sulla scrittura. Qui ho scoperto un punto molto interessante: la velocità di lettura nella versione non DMA era di circa 250-260 kb/s, mentre con DMA era solo 5!!! Inoltre, il consumo di CPU senza utilizzare DMA era del 3% e con DMA - 75-80%!!! Quelli. Il risultato è esattamente l'opposto di quello che ci si aspettava.

Offtop pro 3%

Qui ho avuto un problema divertente con la misurazione del carico del processore: a volte la funzione diceva che il processore era caricato solo per il 3%, sebbene il processore avrebbe dovuto tremare senza fermarsi. In effetti, il carico era del 100% e la mia funzione di misurazione non è stata affatto chiamata: ha la priorità più bassa e semplicemente non c'era abbastanza tempo per farlo. Pertanto, ho ricevuto l'ultimo valore memorizzato prima dell'inizio dell'esecuzione. In condizioni normali, la funzione funziona in modo più corretto.


Avendo sovrapposto il codice del driver con la registrazione di quasi tutte le righe, ho scoperto un problema: ho usato la funzione di callback sbagliata. Inizialmente, ho usato HAL_SPI_Receive_DMA () nel mio codice e, insieme ad esso, il callback HAL_SPI_RxCpltCallback è stato utilizzato in coppia. Questo design non ha funzionato a causa della sfumatura con l'invio simultaneo di 0xff. Quando ho cambiato HAL_SPI_Receive_DMA() in HAL_SPI_TransmitReceive_DMA(), ho dovuto cambiare il callback in HAL_SPI_TxRxCpltCallback() allo stesso tempo. Quelli. infatti la lettura è avvenuta, ma per mancanza di callback, la velocità è stata regolata da un timeout di 100ms.

Dopo aver corretto la richiamata, tutto è andato a posto. Il carico della CPU è sceso al 2,5% (ora onesto) e la velocità è persino aumentata fino a 500 kb / s. È vero, il prescaler doveva essere impostato su 4 - con il prescaler su 2, le asserzioni si riversavano nella libreria SdFat. Sembra che questo sia il limite di velocità della mia carta.

Sfortunatamente, questo non ha nulla a che fare con la velocità di scrittura. La velocità di scrittura era ancora di circa 50-60 kb/s e l'utilizzo della CPU oscillava nell'intervallo del 60-70%. Ma dopo aver scavato per l'intera serata e aver effettuato misurazioni in luoghi diversi, ho scoperto che l'effettiva funzione send() del mio driver (che scrive un settore di 512 byte) funziona in soli 1-2 ms, inclusa l'attesa e la sincronizzazione . A volte, tuttavia, si attiva una sorta di timeout e la registrazione dura 5-7 ms. Ma il problema in realtà non è nel driver, ma nella logica di lavorare con il file system FAT.

Salendo al livello di file, partizioni e cluster, il compito di scrivere 512 su un file non è così banale. Devi leggere la tabella FAT, trovare un posto per il settore scrivibile al suo interno, scrivere il settore stesso, aggiornare le voci nella tabella FAT, scrivere questi settori su disco, aggiornare le voci nella tabella di file e directory e un mucchio di altre cose. In generale, una chiamata a FatFile::write() potrebbe richiedere fino a 15-20 ms, e una buona parte di questo tempo viene impiegata dal lavoro effettivo del processore per elaborare i record nel file system.

Come ho già notato, il carico del processore durante la registrazione è del 60-70%. Ma questo numero dipende anche dal tipo di file system (Fat16 o Fat32), dalla dimensione e, di conseguenza, dal numero di questi cluster sulla partizione, dalla velocità della chiavetta stessa, dall'intasamento e dalla frammentazione del supporto, dall'utilizzo di nomi di file lunghi e molto altro. Quindi, per favore, tratta queste misurazioni come una sorta di figure relative.

Laggiù: USB a doppio buffer

Con questo componente si è rivelato interessante. C'erano una serie di carenze nell'implementazione USB Serial originale di STM32GENERIC e mi sono impegnato a riscriverla per me stesso. Ma mentre studiavo come funziona USB CDC, leggevo il codice sorgente e studiavo la documentazione, i ragazzi di STM32GENERIC hanno notevolmente migliorato la loro implementazione. Ma prima le cose principali.

Quindi, l'implementazione originale non mi andava bene per i seguenti motivi:

  • I messaggi vengono inviati in modo sincrono. Quelli. un banale trasferimento byte per byte di dati dall'UART GPS all'USB attende l'invio di ogni singolo byte. Per questo motivo, il carico del processore può raggiungere fino al 30-50%, che ovviamente è molto (la velocità UART è solo 9600)
  • Non c'è sincronizzazione. Quando si stampano messaggi da più thread, l'output è un pasticcio di messaggi che si sovrascrivono parzialmente a vicenda
  • Troppi buffer di ricezione e invio. Un paio di buffer sono dichiarati in USB Middleware, ma non vengono effettivamente utilizzati. Un altro paio di buffer sono dichiarati nella classe SerialUSB, ma poiché sto usando solo l'output, il buffer di ricezione sta solo sprecando memoria.
  • Infine, l'interfaccia della classe Print mi infastidisce. Se ad esempio voglio visualizzare la stringa “Velocità attuale XXX km/h”, allora devo fare fino a 3 chiamate - per la prima parte della stringa, per il numero e per il resto della stringa. Personalmente, sono più vicino nello spirito al classico printf. Anche i flussi Plus vanno bene, ma devi guardare che tipo di codice viene generato dal compilatore.
Per ora, iniziamo con uno semplice: l'invio sincrono di messaggi, senza sincronizzazione e formattazione. In effetti, ho onestamente sbattuto il codice da STM32GENERIC.

Attuazione `sulla fronte`

esterno USBD_HandleTypeDef hUsbDeviceFS; void usbDebugWrite(uint8_t c) ( usbDebugWrite(&c, 1); ) void usbDebugWrite(const char * str) ( usbDebugWrite((const uint8_t *)str, strlen(str)); ) void usbDebugWrite(const uint8_t *buffer, size_t size ) ( // Ignora l'invio del messaggio se USB non è connesso if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; // Trasmette il messaggio ma non oltre il timeout uint32_t timeout = HAL_GetTick() + 5; while(HAL_GetTick()< timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


Formalmente, questo non è un codice sincrono, perché non attende l'invio dei dati. Ma questa funzione attende l'invio dei dati precedenti. Quelli. la prima chiamata invierà i dati alla porta e uscirà, ma la seconda attenderà che i dati inviati nella prima chiamata vengano effettivamente inviati. In caso di timeout, i dati vengono persi. Inoltre non succede nulla se non è presente alcuna connessione USB.

Naturalmente, questo è solo uno spazio vuoto, perché. questa implementazione non risolve i problemi individuati. Cosa serve per rendere questo codice asincrono e non bloccante? Bene, almeno il buffer. Solo qui quando questo buffer da trasferire?

Penso che valga la pena fare una piccola digressione sui principi dell'USB. Il fatto è che il trasferimento nel protocollo USB può essere avviato solo dall'host. Se il dispositivo ha bisogno di trasferire dati verso l'host, i dati vengono preparati in uno speciale buffer PMA (Packet Memory Area) e il dispositivo attende che l'host raccolga questi dati. La funzione CDC_Transmit_FS() è responsabile della preparazione del buffer PMA. Questo buffer risiede all'interno delle periferiche USB, non nel codice utente.

Onestamente volevo disegnare una bella immagine qui, ma non ho capito come visualizzarla meglio

Ma sarebbe bello implementare il seguente schema. Il codice client, se necessario, scrive i dati in un buffer di archiviazione (utente). Di tanto in tanto arriva l'host e prende tutto ciò che si è accumulato nel buffer fino a questo punto. Questo è molto simile a quello che ho descritto nel paragrafo precedente, ma c'è un avvertimento chiave: i dati sono nel buffer dell'utente, non nel PMA. Quelli. Vorrei fare a meno di chiamare CDC_Transmit_FS(), che versa i dati dal buffer dell'utente in PMA e invece cattura il callback "qui è arrivato l'host, i dati sono richiesti".

Sfortunatamente, questo approccio non è possibile nell'attuale design del middleware USB CDC. Più precisamente, potrebbe essere possibile, ma è necessario approfondire l'implementazione del driver CDC. Non ho ancora abbastanza esperienza nei protocolli USB per farlo. Inoltre, non sono sicuro che i limiti di tempo USB siano sufficienti per un'operazione del genere.

Fortunatamente, in quel momento ho notato che STM32GENERIC aveva già viaggiato in giro per una cosa del genere. Ecco il codice che ho riprogettato in modo creativo da loro.

USB seriale con doppio buffer

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; volatile uint16_t usbTxHead = 0; volatile uint16_t usbTxTail = 0; volatile uint16_t usbTrasmissione = 0; uint16_t translateContiguousBuffer() ( uint16_t count = 0; // Trasmette i dati contigui fino alla fine del buffer if (usbTxHead > usbTxTail) ( count = usbTxHead - usbTxTail; ) else ( count = sizeof(usbTxBuffer) - usbTxTail; ) CDC_Transmit_FS (&usbTxBuffer, count); return count; ) void usbDebugWriteInternal(const char *buffer, size_t size, bool reverse = false) ( // Ignora l'invio del messaggio se USB non è connesso if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; / / Trasmette il messaggio ma non oltre il timeout uint32_t timeout = HAL_GetTick() + 5; // Proteggi questa funzione da ingressi multipli MutexLocker locker(usbMutex); // Copia i dati nel buffer for(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; } }


L'idea alla base di questo codice è la seguente. Sebbene non sia stato possibile catturare la notifica "l'host è arrivato, vuole dati", si è scoperto che era possibile organizzare una richiamata "Ho inviato i dati all'host, puoi versare il prossimo". Si scopre un tale doppio buffer: mentre il dispositivo è in attesa che i dati vengano inviati dal buffer PMA interno, il codice utente può aggiungere byte al buffer di accumulo. Quando l'invio dei dati è completato, il buffer di accumulo viene versato nel PMA. Resta solo da organizzare proprio questa richiamata. Per fare ciò, è necessario archiviare leggermente la funzione USBD_CDC_DataIn()

Middleware USB archiviato

statico uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) ( USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; if(pdev->pClassData != NULL) ( hcdc->TxState = 0; USBSerialTransferCompletedCB(); return USBD_OK(); ) else (restituisce USBD_FAIL; ) )


A proposito, la funzione usbDebugWrite è protetta da un mutex e dovrebbe funzionare correttamente da più thread. Non ho protetto la funzione USBSerialTransferCompletedCB(): viene chiamata da un interrupt e opera su variabili volatili. Francamente, da qualche parte l'insetto cammina ancora qui, i simboli vengono inghiottiti molto occasionalmente. Ma non è fondamentale per me per il debug. Questo non verrà chiamato nel codice di produzione.

Di nuovo lì: printf

Mentre questo pezzo è in grado di funzionare solo con linee costanti. È ora di rovinare l'analogo di printf(). Non voglio usare la vera funzione printf(): trascina 12 kilobyte di codice extra e un heap, che non ho. Ho trovato il mio registro di debug, che una volta ho scritto per l'AVR. La mia implementazione è in grado di stampare stringhe e numeri in formato decimale ed esadecimale. Dopo alcune rifiniture e test, si è rivelato qualcosa del genere:

Implementazione semplificata di printf

// L'implementazione di sprintf richiede più di 10 kb e l'aggiunta di heap al progetto. Penso che questo sia // troppo per la funzionalità di cui ho bisogno // // Di seguito c'è una funzione di dumping simile a printf homebrew che accetta: // - %d per le cifre // - %x per i numeri come HEX // - %s per le stringhe // - %% per il simbolo di percentuale // // L'implementazione supporta anche la larghezza del valore e lo zero padding // Stampa il numero nel buffer (in ordine inverso) // Restituisce il numero di simboli stampati size_t PrintNum(unsigned int value , uint8_t radix, char * buf, uint8_t width, char padSymbol) ( //TODO check negative here size_t len ​​​​= 0; // Stampa il numero do ( char digit = value % radix; *(buf++) = digit< 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value >0); // Aggiungi 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); }


La mia implementazione è molto più semplice di quella della libreria, ma può fare tutto ciò di cui ho bisogno: stampare stringhe, numeri decimali ed esadecimali con formattazione (larghezza del campo, fine del numero con zeri a sinistra). Non stampa ancora numeri negativi e in virgola mobile, ma è facile da aggiungere. Successivamente potrei rendere possibile scrivere il risultato su un buffer di stringhe (come sprintf) e non solo su USB.

Le prestazioni di questo codice sono di circa 150-200 kb/s con trasferimento tramite USB e dipendono dal numero (lunghezza) dei messaggi, dalla complessità della stringa di formato e anche dalla dimensione del buffer. Questa velocità è sufficiente per inviare un paio di migliaia di piccoli messaggi al secondo. Soprattutto, le chiamate non bloccano.

Ancora più stretto: HAL di basso livello

In linea di principio, questo avrebbe potuto finire, ma ho notato che i ragazzi di STM32GENERIC hanno letteralmente versato un nuovo HAL l'altro giorno. È interessante notare che molti file sono apparsi nel nome stm32f1xx_ll_XXXX.h. Hanno trovato un'implementazione alternativa e di livello inferiore di HAL. Quelli. il solito HAL fornisce un'interfaccia di livello abbastanza alto nello stile di "prendi questo array e passamelo attraverso questa interfaccia. Segnala il completamento con un'interruzione. Al contrario, i file con le lettere LL nel nome forniscono un'interfaccia di livello più basso come "imposta questi flag per questo e tale registro".

Mistero della nostra città

Vedendo i nuovi file nel repository STM32GENERIC, ho voluto scaricare il kit completo dal sito Web della ST. Ma googling mi ha portato solo a HAL (STM32 Cube F1) versione 1.4 che non contiene questi nuovi file. Anche il configuratore grafico STM32CubeMX offriva questa versione. Ho chiesto agli sviluppatori di STM32GENERIC dove hanno ottenuto la nuova versione. Con mia sorpresa, ho ricevuto un link alla stessa pagina, solo che ora offriva il download della versione 1.6. Google ha anche iniziato improvvisamente a "trovare" una nuova versione, oltre a un CubeMX aggiornato. Mistico e altro!


Perché è necessario? Nella maggior parte dei casi, un'interfaccia di alto livello fa davvero un buon lavoro. HAL (Hardware Abstraction Layer) giustifica pienamente il suo nome: astrae il codice dal processore e dai registri hardware. Ma in alcuni casi, HAL limita l'immaginazione del programmatore, mentre utilizzando astrazioni di livello inferiore si potrebbe implementare l'attività in modo più efficiente. Nel mio caso, questi sono GPIO e UART.

Proviamo a sentire le nuove interfacce. Cominciamo con le lampadine. Sfortunatamente, non ci sono abbastanza esempi su Internet. Cercheremo di capire il codice nei commenti alle funzioni, poiché tutto è in ordine con questo.

Apparentemente, queste cose di basso livello possono anche essere divise in 2 parti:

  • funzioni leggermente più di alto livello nello stile di un normale HAL: ecco la struttura di inizializzazione per te, per favore inizializza la periferia per me.
  • Setter e getter di livello leggermente inferiore per singoli flag o registri. Per la maggior parte, le funzioni di questo gruppo sono inline e solo intestazione
Per impostazione predefinita, i primi sono disabilitati dalla definizione USE_FULL_LL_DRIVER. Bene, via e al diavolo loro. Useremo il secondo. Dopo un po' di sciamanesimo, ho ottenuto questo driver LED

Morgulka su LL HAL

// Classe da incapsulare lavorando con i LED integrati // // Nota: questa classe inizializza i pin corrispondenti nel costruttore. // Potrebbe non funzionare correttamente se gli oggetti di questa classe vengono creati come variabili globali class LEDDriver ( const uint32_t pin = LL_GPIO_PIN_13; public: LEDDriver() ( // abilita l'orologio sulla periferica GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Init PC 13 as output LL_GPIO_SetPinMode (GPIOC, pin, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetPinOutputType (GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinSpeed ​​​​(GPIOC, pin, LL_GPIO_SPEED_FREQ_LOW);) void turnOn () (LL_GPIO_ResetOutputPin (GPIOC, pin);) void turnOn () (LL_GPIO_ResetOutputPin (GPIOC, pin);) (GPIOC, pin); ) void toggle() ( LL_GPIO_TogglePin(GPIOC, pin); ) ); void vLEDThread(void *pvParameters) ( LEDDriver led; // Lampeggia solo una volta ogni 2 secondi per (;;) ( vTaskDelay(2000); led.turnOn(); vTaskDelay(100); led.turnOff(); ) )


Tutto è molto semplice! La cosa bella è che c'è davvero lavoro direttamente con i registri e le bandiere. Non c'è alcun sovraccarico per il modulo HAL GPIO, che a sua volta compila fino a 450 byte, e il controllo pin da STM32GENERIC, che estrae altri 670 byte. Qui, in generale, l'intera classe con tutte le chiamate è stata incorporata nella funzione vLEDThread, con una dimensione di soli 48 byte!

Non ho gestito la timbratura tramite LL HAL. Ma questo non è critico, perché. chiamare __HAL_RCC_GPIOC_IS_CLK_ENABLED() da un normale HAL è in realtà una macro che imposta solo un paio di flag in determinati registri.

I pulsanti sono altrettanto facili.

Pulsanti tramite LL HAL

// Assegnazione pin const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // Inizializza le cose relative ai pulsanti void initButtons() ( //abilita l'orologio alla periferica GPIOC __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Imposta i pin dei pulsanti LL_GPIO_SetPinMode(GPIOC, SEL_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, SEL_BUTTONGP_PIN, LL_GPIO_PIN, LL_GPIO_PIN, LL_GPIO_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, OK_BUTTON_PIN, LL_GPIO_PULL_DOWN); ) // Lettura dello stato del pulsante (esegui prima il debounce) inline bool getButtonState(uint32_t pin) ( if(LL_GPIO_IsInputPinSet(GPIOC, pin)) ( // lay dobouncing vBOUNDeRATION ); if( LL_GPIO_IsInputPinSet(GPIOC, pin)) restituisce true; ) restituisce false; )


Con UART, tutto sarà più interessante. Lascia che ti ricordi il problema. Quando si utilizza HAL, la ricezione doveva essere "ricaricata" dopo ogni byte ricevuto. La modalità "accetta tutto" non è prevista in HAL. E con LL HAL, dovremmo riuscire.

L'impostazione dei pin mi ha fatto non solo pensarci, ma anche guardare il Manuale di riferimento

Configurazione dei pin UART

// Init pin in modalità funzione alternativa LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE); //Perno TX LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_9, LL_GPIO_SPEED_FREQ_HIGH); LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_9, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_10, LL_GPIO_MODE_INPUT); //Perno RX


Rielaborazione dell'inizializzazione UART su nuove interfacce

Inizializzazione UART

// Prepara per l'inizializzazione LL_USART_Disable(USART1); // Inizia 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); // Useremo l'interrupt UART per ottenere i dati HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Abilita l'interrupt UART alla ricezione dei byte LL_USART_EnableIT_RXNE(USART1); // Abilita infine la periferica LL_USART_Enable(USART1);


Ora interrompi. Nella versione precedente, avevamo fino a 2 funzioni: una gestiva l'interruzione e la seconda era una richiamata (dalla stessa interruzione) sul byte ricevuto. Nella nuova versione, abbiamo configurato l'interrupt per ricevere solo un byte, quindi riceveremo immediatamente il byte ricevuto.

Interruzione UART

// Memorizza il byte ricevuto inline void charReceivedCB(uint8_t c) ( rxBuffer = c; lastReceivedIndex++; // Se viene ricevuto un simbolo EOL, notifica al thread GPS che la riga è disponibile per la lettura if(c == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); ) extern "C" void USART1_IRQHandler(void) ( uint8_t byte = LL_USART_ReceiveData8(USART1); gpsUart.charReceivedCB(byte); )


La dimensione del codice del driver è diminuita da 1242 a 436 byte e il consumo di RAM è diminuito da 200 a 136 (di cui 128 è un buffer). Non male secondo me. L'unico peccato è che questa non è la parte più vorace. Sarebbe possibile archiviare qualcos'altro, ma al momento non sto particolarmente inseguendo il consumo di risorse: le ho ancora. E l'interfaccia HAL di alto livello funziona abbastanza bene nel caso del resto delle periferiche.

Guardando indietro

Sebbene all'inizio di questa fase del progetto fossi scettico sull'HAL, sono comunque riuscito a riscrivere tutto il lavoro con le periferiche: GPIO, UART, I2C, SPI e USB. Ho fatto profondi progressi nella comprensione di come funzionano questi moduli e ho cercato di trasmettere le conoscenze in questo articolo. Ma questa non è affatto una traduzione del Manuale di riferimento. Al contrario, ho lavorato nel contesto di questo progetto e ho mostrato come è possibile scrivere driver periferici in puro HAL.

L'articolo si è rivelato essere una storia più o meno lineare. Ma in effetti, ho generato una serie di brunch in cui ho segato simultaneamente in direzioni esattamente opposte. Al mattino potevo incappare in problemi con le prestazioni di una specie di libreria arduino e decidere fermamente di riscrivere tutto su HAL, e la sera mi accorgevo che qualcuno aveva già depositato il supporto DMA in STM32GENERIC e avevo voglia di tornare indietro . Oppure, ad esempio, un paio di giorni di confronto con le interfacce arduino cercando di capire come sia più conveniente trasferire i dati tramite I2C, mentre su HAL ciò avviene in 2 righe.

In generale, ho ottenuto ciò che volevo. La maggior parte del lavoro periferico è sotto il mio controllo ed è scritto in HAL. Arduino, d'altra parte, funge solo da adattatore per alcune librerie. È vero, c'erano ancora le code. Devi ancora raccogliere il tuo coraggio e rimuovere STM32GENERIC dal tuo repository, lasciando solo un paio di classi davvero necessarie. Ma tale pulizia non si applicherà più a questo articolo.

Per quanto riguarda Arudino e i suoi cloni. Mi piace ancora questo quadro. Con esso, puoi prototipare rapidamente qualcosa senza davvero preoccuparti di leggere manuali e schede tecniche. Con arduino, in linea di principio, puoi realizzare anche terminali se non ci sono particolari requisiti di velocità, consumo o memoria. Nel mio caso, questi parametri sono molto importanti, quindi ho dovuto passare a HAL.

Ho iniziato a lavorare su stm32duino. Questo clone merita davvero attenzione se vuoi avere un "arduino" su STM32 e fare in modo che tutto funzioni immediatamente. Inoltre, monitorano da vicino il consumo di RAM e flash. Al contrario, lo stesso STM32GENERIC è più spesso e si basa su un mostruoso HAL. Ma questo quadro si sta sviluppando attivamente e sembra più finito. In generale, posso consigliare entrambi i framework con una leggera preferenza per STM32GENERIC per HAL e uno sviluppo più dinamico al momento. Inoltre, Internet è pieno di esempi per HAL e puoi sempre modificare qualcosa per te stesso.

Tratto ancora HAL stesso con un certo grado di disgusto. La libreria è troppo ingombrante e brutta. Prendo atto del fatto che la libreria è sish, il che porta all'uso di nomi e costanti di funzioni lunghi. Tuttavia, questa non è una libreria con cui è divertente lavorare. Piuttosto, è una misura forzata.

Ok, l'interfaccia - anche gli interni ti fanno pensare. Funzioni enormi con funzionalità per tutte le occasioni comportano uno spreco di risorse. Inoltre, se puoi combattere con codice extra nella flash usando l'ottimizzazione del tempo di collegamento, l'enorme consumo di RAM viene trattato solo riscrivendo su LL HAL.

Ma non è nemmeno frustrante, ma in alcuni punti è solo un disprezzo per le risorse. Quindi ho notato un enorme sovraccarico di memoria nel codice USB Middleware (formalmente questo non è un HAL, ma viene fornito con STM32Cube). Le strutture usb occupano 2,5kb di memoria. Inoltre, la struttura USBD_HandleTypeDef (544 byte) ripete in gran parte il PCD_HandleTypeDef dal livello inferiore (1056 byte) - in essa sono definiti anche gli endpoint. I buffer del ricetrasmettitore vengono inoltre dichiarati in almeno due posizioni: USBD_CDC_HandleTypeDef e UserRxBufferFS/UserTxBufferFS.

I descrittori sono generalmente dichiarati nella RAM. Per che cosa? Sono costanti! Quasi 400 byte di RAM. Fortunatamente, alcuni dei descrittori sono ancora costanti (poco meno di 300 byte). I descrittori sono informazioni immutabili. E qui c'è un codice speciale che li patch, e, ancora, con una costante. Sì, e uno che è già iscritto lì. Funzioni come SetBuffer per qualche motivo accettano un buffer non costante, che impedisce anche ai descrittori e ad altre cose di essere inseriti in flash. Qual è la ragione? Si risolve in 10 minuti!

Oppure, la struttura di inizializzazione fa parte dell'handle dell'oggetto (ad esempio, i2c). Perché memorizzarlo dopo l'inizializzazione della periferia? Perché ho bisogno di puntatori a strutture inutilizzate, ad esempio perché ho bisogno di dati associati a DMA se non li uso?

Duplica anche il codice.

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


Una conversione speciale in "tipo unicode", che potrebbe essere eseguita in fase di compilazione. Inoltre, per questo viene assegnato un buffer speciale.

Dati const beffardi

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


Non fatale, ma ti chiedi se HAL sia bravo come scrivono gli apologeti? Bene, questo non è quello che ti aspetti da una libreria di un produttore e progettata per i professionisti. Questi sono microcontrollori! Qui le persone salvano ogni byte e ogni microsecondo è prezioso. E qui, capisci, un buffer per mezzo chilo e la conversione di stringhe costanti al volo. Va notato che la maggior parte delle osservazioni si riferisce a USB Middleware.

UPD: in HAL 1.6, anche il callback I2C DMA Transfer Completed è stato interrotto. Quelli. lì è sparito del tutto il codice che, nel caso di invio di dati tramite DMA, genera conferma, sebbene sia descritto nella documentazione. C'è una ricezione, ma nessuna trasmissione. Ho dovuto tornare a HAL 1.4 per il modulo I2C, poiché esiste un modulo: un file.

Infine, darò il consumo di flash e RAM dei vari componenti. Nella sezione Driver, ho elencato i valori per entrambi i driver basati su HAL e LL HAL. Nel secondo caso, le sezioni corrispondenti della sezione HAL non vengono utilizzate.

Consumo di memoria

Categoria sottocategoria .testo .rodata .dati .bss
Sistema vettore di interruzione 272
gestori ISR ​​fittizi 178
libc 760
matematica galleggiante 4872
peccato/cos 6672 536
principale&ecc 86
Il mio codice Il mio codice 7404 833 4 578
stampa f 442
Caratteri 3317
NeoGPS 4376 93 300
FreeRTOS 4670 4 209
Adafruit GFX 1768
Adafruit SSD1306 1722 1024
SdFat 5386 1144
Middleware USB Nucleo 1740 333 2179
Centro per la prevenzione e il controllo delle malattie 772
Autisti UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Pulsanti LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
misc 28 24
Stampa 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
heap (non molto utilizzato) 1068
Heap di FreeRTOS 10240

È tutto. Sarò lieto di ricevere commenti costruttivi, nonché consigli se qualcosa può essere migliorato qui.

tag:

  • HAL
  • STM32
  • Cubo STM32
  • arduino
Aggiungi i tag

Un elenco di articoli che aiuteranno anche un principiante a studiare il microcontrollore STM32. Dettagli di tutto con esempi che vanno dal lampeggio di un LED all'azionamento di un motore brushless. Gli esempi utilizzano la Standard Peripheral Library (SPL).

Scheda di test STM32F103, programmatore ST-Link e firmware per Windows e Ubuntu.

VIC (controllore di interrupt vettorizzato annidato) - modulo di controllo degli interrupt. Impostazione e utilizzo degli interrupt. Interrompere le priorità. Interruzioni nidificate.

ADC (convertitore analogico-digitale). Schema di alimentazione ed esempi di utilizzo dell'ADC in varie modalità. Canali regolari e iniettati. Utilizzo di ADC con DMA. Termometro interno. Watchdog analogico.

Timer per uso generale. Generazione di un interrupt a intervalli regolari. Una misura del tempo tra due eventi.

Cattura di un segnale con un timer sull'esempio di lavoro con un sensore a ultrasuoni HC-SR04

Utilizzo del timer per lavorare con l'encoder.

Generazione PWM. Controllo della luminosità del LED. Servocomando (servi). Generazione del suono.

Ho fatto notare che la libreria standard è collegata al sistema. Infatti, CMSIS è collegato - un sistema di rappresentazione strutturale generalizzato per MK, così come SPL - una libreria periferica standard. Consideriamo ciascuno di essi:

CMSIS
È un insieme di file di intestazione e un piccolo insieme di codice per unificare e strutturare il lavoro con il core e le periferiche di MK. Infatti, senza questi file è impossibile lavorare normalmente con MK. Puoi ottenere la libreria sulla pagina per MK.
Questa libreria, secondo la descrizione, è stata creata per unificare le interfacce quando si lavora con qualsiasi MK della famiglia Cortex. Tuttavia, in realtà, si scopre che questo è vero solo per un produttore, ad es. passando al microcontrollore di un'altra azienda, sei costretto a studiarne la periferia quasi da zero.
Sebbene quei file che si riferiscono al core del processore MK siano identici per tutti i produttori (se non altro perché hanno un modello di core del processore, fornito sotto forma di blocchi IP da ARM).
Pertanto, lavorare con parti del kernel come registri, istruzioni, interruzioni e blocchi del coprocessore è standard per tutti.
Per quanto riguarda le periferiche, STM32 e STM8 (all'improvviso) sono quasi simili, e questo vale in parte anche per altri MK rilasciati da ST. Nella parte pratica mostrerò quanto sia facile usare CMSIS. Tuttavia, le difficoltà nell'utilizzarlo sono associate alla riluttanza delle persone a leggere la documentazione e comprendere il dispositivo MK.

SPL
Libreria periferica standard - libreria periferica standard. Come suggerisce il nome, lo scopo di questa libreria è creare un'astrazione per la periferia MK. La libreria è costituita da file di intestazione in cui vengono dichiarate costanti comprensibili dall'uomo per la configurazione e l'utilizzo delle periferiche MK, nonché file di codice sorgente assemblati nella libreria stessa per le operazioni con le periferiche.
SPL è un'astrazione su CMSIS, che presenta all'utente un'interfaccia comune per tutti gli MK, non solo di un produttore, ma in generale per tutti gli MK con un core del processore Cortex-Mxx.
Si ritiene che sia più conveniente per i principianti, perché. permette di non pensare a come funzionano le periferiche, tuttavia la qualità del codice, l'universalità dell'approccio e la rigidità delle interfacce impongono allo sviluppatore alcune restrizioni.
Inoltre, la funzionalità della libreria non sempre consente di implementare con precisione la configurazione di alcuni componenti come USART (Universal Synchronous-Asynchronous Serial Port) in determinate condizioni. Nella parte pratica, descriverò anche come lavorare con questa parte della libreria.

Articoli correlati in alto