Eclipse - GCC ARM - STM32 - Standard Peripheral Library

Comment utiliser Eclipse pour développer sur STM32. Maintenant que j'ai réussi à obtenir un setup stable et fiable, voici un petit tuto pour avoir un setup fonctionnel de développement.

Avant-propos

Comme je l'ai dit dans un post précédent, j'utilise beaucoup MBED. Maintenant je me retrouve obligé d'y trouver une alternative. Pour plusieurs raisons:

  • L'IDE est online - Donc pas de réseau ou patch day => pas de dev
  • L'IDE est buggée - Quoique j'ai découvert que mes problèmes de clavier disfonctionnel étaient dus à Random Agent Spoofer, et donc sont réglés maintenant
  • Les mises à jour sont aléatoires, et incompréhensibles, en tous cas moi je ne comprend pas comment les mises à jour ont lieu
  • J'aime pas le C++
  • L'archi est figée, donc si elle ne convient pas, on est mort
  • Les conflits de ressources n'apparaîssent pas avant l'exécution, donc beaucoup de perte de temps à faire des tests préliminaires permettant de vérifier que les ressources sont bien assignées
  • Cette lib prend un max de place en Flash

Récemment, les deux derniers points ont été bloquants. J'ai développé une carte utilisant une Nucleo 32. J'ai estimé que, vu ce que je voulais faire, Nucleo-F042K6 était suffisante. Le MCU dessus a 32kB de Flash. Or, MBED ne rentre pas. Genre pas dut out, si je mets le moindre périphérique autre que des GPIO, ça ne tient plus en Flash. Et je n'ai même pas encore développé l'appli ! Les librairies de base seules bouffent plus que 32kB !

Donc il fallait que je trouve une autre solution. La solution la plus logique, c'est d'utiliser la lib standard fournie par ST. Parce que je n'ai pas le temps de chercher une lib alternative qui me conviendrait, et encore moins le temps de développer mes propres drivers.

En attendant, pour faire mes tests hardware sur la carte en question toute en utilisant MBED pour gagner du temps, je suis monté en gamme pour trouver une carte qui me permette de faire mes tests. Pour faire rentrer tous les périphériques dont j'ai besoin, j'ai été obligé de prendre au plus gros modèle : Nucleo-L432KC. 256kB de Flash, quand-même ! En tous cas dans la Nucleo-F303K8 (64kB de Flash) ça ne rentre pas.

Pourquoi Eclipse ? Parce que malgré tout ça reste moins pénible à utiliser que GCC en ligne de commande avec Make. Oui, j'assume être un mouse-fag. Et je vous emmerde.

Je vais quand-même faire un gros apparté sur l'installation d'Eclipse, parce que ça n'a rien de naturel, même si c'est trivial en fait. Mais il faut le gérer.

Installation d'Eclipse

Eclipse marche avec Java, donc il faut installer Java.

Windows

Java sous WIndows, c'est la merde. Souvent, le JRE est installé "tout seul" à cause du navigateur. Et souvent, ce n'est pas la bonne version qui est installée. Il faut vérifier ça, les seules règles étant que:

  • La version de Java doit être compatbiel avec la version d'Eclipse qu'on veut utiliser (duh)
  • La variante (32 bits / 64 bits) de Java doit correspondre à l'OS et à Eclipse. Donc Windows 32 => Java 32 => Eclipse 32, sinon Windows 64 => Java 64 => Eclipse 64. Cohérence avant tout. Il doit être possible d'utiliser Java + Eclipse 32 sur Windows 64, mais bon, je ne vois pas trop l'intérêt. Je ne couvrirai pas ce cas.
    Donc, si il y a incohérence, il faut désinstaller Java et le réinstaller :

http://www.oracle.com/technetwork/j...

Ensuite il faut installer Eclipse:

https://www.eclipse.org/downloads/

Aujourd'hui (04/2017) je prends Neon.

Là il est fort probable que Eclipse refuse de démarrer avec un message d'erreur long et incompréhensible. La plupart du temps, c'est lié au fait que Eclipse ne trouve pas Java. Il faut alors aller modifier le fichier de config d'Eclipse pour lui donner le bon chemin d'accès. Ce fichier de config c'est eclipse.ini qui est à la racine de l'install d'Eclipse. Dans ce fichier il y a une ligne -vm avec dans la ligne suivante le chemin d'accès à l'exécutable de Java, par exemple C:/Program Files/Java/jre1.8.0_131/bin/javaw.exe . Il faut trouver où est installé Java et faire pointer sur l'exécutable javaw.exe.

Et donc, attention, en cas de mise à jour de Java - qui est assez automatique - il faut mettre à jour le chemin d'accès à javaw.exe - qui n'est pas du tout automatique. Car La mise à jour de Java a la facheuse tendance à changer les noms des répertoires d'install. Par exemple, lorsque la mise à jour du de JRE 1.8.0.131 a remplacé JRE 1.8.0.121, le répertoire a changé de nom, et donc il a fallu que j'édite eclipse.ini pour qu'Eclipse accepte de se démarrer.

Plug-ins

Une fois Eclipse lancé, il faut installer des plug-ins pour les STM32. Il y a deux familles de plug-ins que j'ai réussi à installer et qui me paraissent bien:

  • GNU ARM Eclipse
  • AC6 System Workbench GNU ARM Eclipse est un grand classique, on trouve plein de docs dessus. Ce plug-in permet de créer des projets pré-configurés pour un modèle particulier de MCU (par exemple STM32L152RE).

AC6 est développé par la boîte de formation du même nom. Ils proposent même un Eclipse pré-packagé, à télécharger sur leur site. Par contre, il faut faire un compte sur leur site, alors que l'installation du plug-in depuis Eclipse ne le requiert pas. Donc plutôt installer depuis Eclipse. Ce plug-in supporte moins de MCUs que GNU ARM Eclipse, mais apporte la possibilité de créer des projets pré-configurés aussi pour un bon paquet de cartes d'éval - dont les Nucleo - ce qui le rapproche beaucoupo de MBED, pour le coup.
Je pense que GNU ARM Eclipse est préférable, car moins lié à une entité unique, mais le plug-in AC6 permet de gagner du temps avec la config spécifique aux cartes, donc bon, l'on fera comme on le souhaitera.

Pour installer des plugins : menu Help / Install New Software ...
Ensuite il faut indiquer des repositories, en faisant Add ...
Pour GNU ARM Eclipse : GNU ARM Eclipse Plug-ins - http://gnuarmeclipse.sourceforge.net/updates
Pour AC6 : System Workbench for STM32 - Bare Machine edition - http://www.openstm32.org/Eclipse-updates/org.openstm32.system-workbench.site

Ensuite il faut cocher les sous-modules qui vont bien, à savoir en gros tout ce qui contient "STM32" ou "ARM" ou "Cortex-M" ou "cross-compiler" dans son titre, puis faire install / next ...
L'avantage de ces deux plug-ins, c'est qu'ils permettent d'importer directement la lib standard quand on créé un projet, donc pas besoin de s'emmerder à monter les cources, c'est fait automatiquement. Pratique.
Une fois l'un et / ou l'autre plug-in installé, on créé un projet en faisant File / New ... C Project. On lui met un petit nom, puis on choisit le type de projet; dans la catégorie "Executable":
New C Project

Si on choisit STM32Fxxx C/C++ project, on va créer un projet par GNU ARM Eclipse, donc on configure le MCU uniquement.
Si on choisit Ac6 STM32 MCU project, on va créer un projet par AC6 System Workbench, donc on va choisir le MCU et la carte sur laquelle il se trouve.

En toute honnêteté, les configs pour les Nucleo sont peu utiles, donc je ne m'en suis servi que comme exemple pour faire mes modules.

DEBUG

Ce qu'il y a de bien avec les Nucleo et le ST-link intégré, c'est qu'on peut débugger. Attention, il ne faut aps oublier d'installer le driver des ST-link v2, sinon les sessions de debug ne démarreront pas.
http://www.st.com/content/st_com/en...
Ensuite on fait menu Run/Debug COnfigurations ... ou on clique sur le petit scarabée. On clique sur le typed e session qu'on veut, par exempleAc6 STM32 Debugging, clic-droit/New..., puis apply (en bas à droite) et Debug. Si on a pas oublié de brancher la carte, il va se connecter, charger le SW dans la carte (la LED sur les Nucleo se met à clignoter rouge/vert), et placer le compteur programme sur la première instruction du main(). On fait run et c'est parti ! Eclipse va proposer d'ouvrir la perspective de debug, choisir Yes, c'est plus lisible.
Je n'ai pas trouvé de façon simple de faire un reset. Perso je fais Run/Terminate and Relaunch.
Si on fait des modifs dans le code et qu'on recompile, il faut faire Run/Terminate and relaunch pour recharger le code. Sachant que si on fait ça sans compiler, il va recompiler automatiquement de toutes façons.
On double-clique sur la bande à gauche des numéros de ligne pour ajouter des breakpoints. Attention, avec le ST-link on est limités à 4 ou 6 bre"akpoints en fonction des cas, donc ne pas en abuser.
Dans la perspective de debug, en hautà droite il y a un onglet I/O Registers qui permet de voir et déditer les registres. Pour changer la valeur d'un registre il faut être en halt - donc avoir arrêté le processeur sur un breakpoint. Pour voir un registre, il faut le sélectionner, clic-droit/Activate, son contenu apparaît en vert. Pour le modifier, cliquer sur la valeur, on voit des petits boutons qui apparaîssent, un par bit, on clique pour changer la valeur, et on clique sur le petit bouton set pour valider la modif. Ou alors on clique sur la vlauer de tout le registre pour l'éditer.
En bas il y a un onglet Memory dans lequel on peut afficher la mémoire du MCU. Et en haut à droite l'onglet Expressions permet de mettre des watch sur des variables (locales ou globales).

STM32 Standard Peripheral Library

La lib standard est déclinée par sous-groupe de processeurs, et est disponible sous forme de sources. Dans le zip de la lib il y a aussi plein d'exemples basiques qui permettent de voir comment on met en oeuvre les périphériques. C'est très utile. Il y a aussi des docs qui vont avec, par exemple :
http://www.st.com/content/ccc/resou...
http://www.st.com/content/ccc/resou...
http://www.st.com/content/ccc/resou...

En farfouinant dans les ressources SW des pages du site de ST, on trouve les manquantes. Par exemple pour les STM32L152:
http://www.st.com/content/st_com/en...
Sur la page du STM32152RE, onglets Design / Embedded Software, il y a un des item qui s'appelle "STM32L1xx standard peripherals library", c'est celui-là, et il y a la doc. Et bien entendu on ne le trouve pas en cherchant "STM32L1xx standard library" dans le moteur de recherche, ça serait trop facile ...

Concernant la lib en elle-même, elle est plutôt bas-niveau, mais permet de gagner pas mal de temps si on a pas le courage de se plonger dans l'intégralité des registres de tous les périphériques. Honnêtement, après avoir fait des GPIO, de la SPI, des ADC et du DMA avec, j'ai de toutes façons été obligé de regarder les registres pour le debug, donc ça ne m'a pas fait ganger du temps de façon décisive, mais bon, c'est toujours ça de pris.

En gros, pour chaque périphérique que l'on va utiliser, il va falloir l'initialiser avec une fonction XXX_Init(), qui prend en argument une structure qui va contenir les paramètres de config que l'on veut. Il faut aussi activer l'horloge du périphérique en question (avec les fonctions RCC_AHBPeriphClockCmd, RCC_APBPeriphClockCmd, etc), et activer le périphérqieu avec la fonction xxxPeriphClockCmd(ENABLE). Dans les package des libs standard il y a des exemples dans le répertoire Project/SMT32XXX_StdPeriph_Examples.

Ces libs ne sont pas aussi clé-en-main que MBED. Il faut par exemple au minimum connaître l'archi des horloges pour les configurer correctement. Heureusement c'est asez bien décrit dans le User's Manual (je trouve, par rapport à d'autres MCUs sur lesquels j'ai bossé) et les fonctions dans le module RCC sont suffisament explicites pour qu'on comprenne ce qu'il faut faire. J'ai vu quelques incohérences entre les docs et les commentaires dans les sources de la lib, donc se méfier. A priori les docs - et surtout datasheet + User's Manual - font foi.

Par exemple, sur un STM32F042K6:
http://www.st.com/content/st_com/en...
Le manuel de référence / User's Manual:
http://www.st.com/resource/en/refer...
La datasheet:
http://www.st.com/resource/en/datas...
Et la lib adaptée:
http://www.st.com/content/st_com/en...
A chopper en cliquant sur le bouton "Get Software" en bas.
Si on regarde dans le Reference Manual, chapitre Reset and CLock Control (RCC), page 97 on a une archi des horloges. En gros il y a une source à sélectionner, une PLL à activer et configurer, ou bien un prescaler si on ne veut pas utiliser de PLL (sur certaines applications la précision d'une PLL est insuffisante, par exemple du CAN sur bagnole). Cette source va ensuite générer l'horloge SYSCLK, l'AHB pour cadencer le CPU, et APB pour les périphériques. Sur les processeurs plus gros on peut avoir plusieurs APB avec des pre-scalers distincts, ce qui permet d'avoir des fréquences d'horloge différentes pour certains périphériques.
On voit qu'il y a un oscillateur intégré à 48MHz (HSI48, dispo sur les STM32F04x, ce qui est écrit dans le manuel, sachant que les commentaires du code ça dit que ce n'est que sur les STM32F07x. J'ai fait la manip, c'est le manuel qui a raison : il y a bien un oscillateur à 48MHz sur le STM32F042K6 :). Il y a des prescalers, mais dans le texte ça dit que la fréquence max des horloges AHB et APB est 48MHz, donc ne nous emmerdons pas et mettons tout à 1.
Le SYSCLK va être utilisé pour générer l'interruption principale qui va servir à lancer les tâches applicatives cycliques. Je la met à 1ms en général. Ca donnerait les bouts de codes suivants:

  • Dans le main() pour initialiser les horloges:
 /* SysTick end of count event each 1ms */
 RCC_GetClocksFreq(&RCC_Clocks);
 /* Change system clock to 48MHz internal clock */
 RCC_HSI48Cmd(ENABLE);
 RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI48);
 RCC_GetClocksFreq(&RCC_Clocks); // Refresh system clock frequency
 SysTick_Config(RCC_Clocks.HCLK_Frequency / 1000);


A partir de là la lib standard fournit une fonction "intégrée" SysTickHandler, qui sera automatiquement appelée à la fréquence donnée en paramètre de SysTickConfig. Ici on récupère la fréquence du HSI48 qu'on a précédemment configuré, et on la divise par 1000 pour avoir une occurence à 1ms. Donc on peut ajouter la fonction suivante après le main:

 void SysTick_Handler(void)
 {
   static uint16_t counter_10ms = 0U;
 
   Task_1ms();
 
   /* Scheduler */
   counter_10ms++;
 
   if (counter_10ms == 10U) {
     counter_10ms = 0U;
     Task_10ms();
   }
 }


Comme ça en plus je créée une fonction à 10ms.

  • Pour initialiser des GPIOs:
 /* This functions follows document "Description of STM32F37xx-38xx Standard Peripheral Library section 13.2.1 */
 GPIO_InitTypeDef  GPIO_InitStructure;
 
 /* MODE_IN input */
 // Enable peripheral clocks
 RCC_AHBPeriphClockCmd(PHA_IN_MODE_IN_GPIO_CLK, ENABLE);
 // Configure the GPIO pins
 GPIO_InitStructure.GPIO_Pin = PHA_IN_MODE_IN_PIN;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
 GPIO_Init(PHA_IN_MODE_IN_GPIO_PORT, &GPIO_InitStructure);
 
 /* Outputs */
 
 /*LED1*/
 
 // Enable peripheral clock
 RCC_AHBPeriphClockCmd(PHA_OUT_LED1_GPIO_CLK, ENABLE);
 
 // Configure the GPIO pins
 GPIO_InitStructure.GPIO_Pin = PHA_OUT_LED1_PIN;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(PHA_OUT_LED1_GPIO_PORT, &GPIO_InitStructure);


Et on utilise les fonctions genre GPIO_WriteBit pour écrire les sorties, GPIO_ReadInputDataBit pour lire les entrées.

  • Pour la SPI:
 /* SPI initialisation */
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
 RCC_AHBPeriphClockCmd(SPI_SCK_GPIO_CLK | SPI_MOSI_GPIO_CLK | SPI_MISO_GPIO_CLK, ENABLE);
 // Configure GPIO alternate function
 GPIO_PinAFConfig(SPI_SCK_GPIO_PORT, SPI_SCK_SOURCE, SPI_SCK_AF);
 GPIO_PinAFConfig(SPI_MOSI_GPIO_PORT, SPI_MOSI_SOURCE, SPI_MOSI_AF);
 GPIO_PinAFConfig(SPI_MISO_GPIO_PORT, SPI_MISO_SOURCE, SPI_MISO_AF);
 GPIO_init_T.GPIO_Mode = GPIO_Mode_AF;
 GPIO_init_T.GPIO_OType = GPIO_OType_PP;
 GPIO_init_T.GPIO_PuPd = GPIO_PuPd_DOWN;
 GPIO_init_T.GPIO_Speed = GPIO_Speed_Level_3;
 
 GPIO_init_T.GPIO_Pin = SPI_SCK_PIN;
 GPIO_Init(SPI_SCK_GPIO_PORT, &GPIO_init_T);
 
 GPIO_init_T.GPIO_Pin = SPI_MOSI_PIN;
 GPIO_Init(SPI_MOSI_GPIO_PORT, &GPIO_init_T);
 
 GPIO_init_T.GPIO_Pin = SPI_MISO_PIN;
 GPIO_Init(SPI_MISO_GPIO_PORT, &GPIO_init_T);
 
 SPI_I2S_DeInit(SPI1);
 
 SPI_config.SPI_DataSize = SPI_DataSize_8b;
 SPI_config.SPI_Direction = SPI_Direction_1Line_Tx;
 //SPI_config.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
 SPI_config.SPI_Mode = SPI_Mode_Master;
 //SPI_config.SPI_CPOL = SPI_CPOL_High;
 SPI_config.SPI_CPOL = SPI_CPOL_Low;
 SPI_config.SPI_CPHA = SPI_CPHA_1Edge;
 SPI_config.SPI_NSS = SPI_NSS_Soft;
 SPI_config.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
 SPI_config.SPI_FirstBit = SPI_FirstBit_MSB;
 SPI_Init(SPI1, &SPI_config);
 
 /* Enable the SPI peripheral */
 SPI_Cmd(SPI1, ENABLE);


Et on envoie un message en utilisant SPI_SendData8. A noter qu'ici les chip select sont gérés en SW comme des GPIOs, mais il est possible de les gérer en HW.

  • Pour les ADC, sur ce processeur il n'y a qu'un seul registre de sortie pour tous les canaux. Si on fait de l'acquisition continue il faut donc obligatoirement utiliser du DMA. Heureusement encore, le DMA est plutôt simple à configurer sur ces MCUs, en tous cas la config spécifique pour les ADC en acquisition continue est très simple. Pour les ADC:
 /* GPIOC Periph clock enable */
 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
 
 /* ADC1 Periph clock enable */
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
 
 /* Speed pot input */
 GPIO_init_T.GPIO_Pin = PHA_ADCSPEED_GPIO_PIN;
 GPIO_init_T.GPIO_Mode = GPIO_Mode_AN;
 GPIO_init_T.GPIO_PuPd = GPIO_PuPd_NOPULL;
 GPIO_Init(PHA_ADCSPEED_GPIO_PORT, &GPIO_init_T);
 
 /* Blend pot input */
 GPIO_init_T.GPIO_Pin = PHA_ADCBLEND_GPIO_PIN;
 GPIO_Init(PHA_ADCBLEND_GPIO_PORT, &GPIO_init_T);
 
 /* Peak input */
 GPIO_init_T.GPIO_Pin = PHA_ADCPEAK_GPIO_PIN;
 GPIO_Init(PHA_ADCPEAK_GPIO_PORT, &GPIO_init_T);
 
 /* ADCs DeInit */
 ADC_DeInit(ADC1);
 
 /* Initialize ADC structure */
 ADC_StructInit(&ADC_init_T);
 
 ADC_init_T.ADC_Resolution = ADC_Resolution_12b;
 ADC_init_T.ADC_ContinuousConvMode = ENABLE;
 ADC_init_T.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
 ADC_init_T.ADC_DataAlign = ADC_DataAlign_Right;
 ADC_init_T.ADC_ScanDirection = ADC_ScanDirection_Upward;
 ADC_Init(ADC1, &ADC_init_T);
 
 /* Convert the ADC1 Channel 11 with 239.5 Cycles as sampling time */
 ADC_ChannelConfig(ADC1, PHA_ADCSPEED_CHANNEL , ADC_SampleTime_239_5Cycles);
 ADC_ChannelConfig(ADC1, PHA_ADCBLEND_CHANNEL , ADC_SampleTime_239_5Cycles);
 ADC_ChannelConfig(ADC1, PHA_ADCPEAK_CHANNEL , ADC_SampleTime_239_5Cycles);
 
 /* ADC Calibration */
 ADC_GetCalibrationFactor(ADC1);
 
 /* ADC DMA request in circular mode */
 ADC_DMARequestModeConfig(ADC1, ADC_DMAMode_Circular);
 
 /* Enable ADC_DMA */
 ADC_DMACmd(ADC1, ENABLE);
 
 /* Enable the ADC peripheral */
 ADC_Cmd(ADC1, ENABLE);
 
 /* Wait the ADRDY flag */
 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_ADRDY));
 
 /* ADC1 regular Software Start Conv */
 ADC_StartOfConversion(ADC1);


Et pour le DMA:

 DMA_InitTypeDef   DMA_InitStructure;
 /* DMA1 clock enable */
 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1 , ENABLE);
 /* DMA1 Channel1 Config */
 DMA_DeInit(DMA1_Channel1);
 //DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADC1_DR_Address;
 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(ADC1->DR));
 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RegularConvData_Tab;
 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
 DMA_InitStructure.DMA_BufferSize = PHA_ADCChannelNb;
 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
 DMA_InitStructure.DMA_Priority = DMA_Priority_High;
 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
 DMA_Init(DMA1_Channel1, &DMA_InitStructure);
 /* DMA1 Channel1 enable */
 DMA_Cmd(DMA1_Channel1, ENABLE);


Avec ce code je gère trois entrée analogiques dont les résultats sont écrits en DMA dans la tableau RegularConvData_Tab. DMA_BufferSize donne le nombre de résultats à transférer, c'est donc le nombre de canaux ADC, donc le nombre d'entrées ADC (ici 3, donc).

Voilà, à partir de là on peut commencer à avoir un truc qui tourne.

Add ping

Trackback URL : http://blog.randagodron.eu/index.php?trackback/59

Page top