Requiem-Projects.com

Suite à de nombreuses demandes sur la mise à disposition de mon code pour le ili9341, voici un petit article très court sur le sujet.

En effet, J'ai voulu, pour prototyper rapidement mon compteur de vitesse de voiture, utiliser la librairie très bien écrite de Tilen depuis le site stm32f4-discovery

Or, à l'utilisation, ça marche très bien, mais cette librairie est très lente, surtout quand le bus SPI n'est pas violent, comme sur la carte STM32F401RE.

Cette lenteur est due à une non utilisation du DMA sur le STM32.
Tilen à fait évoluer son code sur ma demande et recommandation, mais il semble malgré tout que la mise en place du DMA par Tilen ne soit pas complètement optimisée (sans aucun reproche péjoratif, il a fait un super boulot).

Donc j'ai décidé de prendre le taureau par les cornes et de faire un code d'exemple pour l'utilisation du DMA avec le ili9341.

J'ai déjà écrit un article à ce sujet, qui montre le comparatif technique et les résultats en fonction des différents essais de DMA. Je ne retraiterai pas de ce sujet et vous pourrez trouver cet article ici

Nous allons voir maintenant la partie logicielle en elle même

Principe


Le principe du DMA est de faire un transfert de données entre un périphérique et un autre sans demander de ressource du micro-contrôleur et de manière autonome. Ainsi le micro-contrôleur peut faire d'autres tâches entre temps et les temps de transfert n'ont pas de temps d'attente.

Pour ma part j'ai choisi de faire des transferts d'un buffer mémoire vers le SPI.

Sur ma carte STM32F401RE, j'utilise le SPI 1 pour le ili9341. Mes explications seront donc basées sur ce postulat. Si vous utilisez un autre port SPI, il y aura sûrement des adaptations à faire manuellement, car mon code n'est pas une librairie générique, mais une base pour mettre en place un DMA sur SPI.

Comme indiqué dans cet article, le ili9341 attend dans un premier temps une commande, puis des datas si nécessaire.

Partant de ce postulat, mon mode de fonctionnement par DMA est le suivante :

  • 1/ j'envoie d'abord la commande au ili9341
  • 2/ puis je remplis un buffer de 2k octets avec les données
  • 3/ je lance le DMA pour 2k octets et j'attends la fin du DMA
  • 4/ Si mes données sont supérieures à 2k octets, alors je remplis à nouveau le buffer et je reprends à l’étape 3
  • 5/ et ainsi de suite jusqu'à la fin de toutes les données

Vous allez me dire : "C'est pas full optimisé : tu fais des paquets de 2ko et en plus tu attends la fin du DMA, donc tu ne fais rien entre temps !"

Je suis d'accord. Mais rien n’empêche d'augmenter la taille du buffer, et aussi de modifier le code pour permettre de faire du traitement pendant le DMA.

Dans mon cas, ce fonctionnement améliore déjà considérablement la vitesse d'affichage, puisque, si vous lisez mon article sur le comparatif DMA, le transfert sans DMA demande au STM32 de faire quelques instructions pour recharger le SPI, alors qu'avec le DMA, le rechargement est immédiat et automatique, donc c'est déjà plus rapide.

Pour le DMA, une fois lancé, il se débrouille tout seul comme un grand. cool

Le STM32 sera lors averti de la fin du DMA par la montée d'un flag par l'interruption TX du DMA. Ce flag signalera la fin du processus du DMA. Cette interruption sert également de finalisation propre du DMA (interruption, flag de status, etc...)

Autre remarque : Pourquoi envoyé la commande indépendamment du buffer du DMA ?
Tout simplement pour des raisons pratiques : la commande est unique et en plus il faut gérer la broche D/C à 0 pour la commande et ensuite à 1 pour les données. Donc le DMA est très très bête : il envoie ce qu'on lui donne. Il ne sait pas gérer de lui même le changement d’état de la broche D/C. S'il faut mettre en place une interruption DMA pour dire "c'est le premier octet de commande, donc D/C=0 et après D/C=1", c'est perdre ici du temps. Par conséquent, le DMA ne sert que pour le transfert massif de données, donc des datas graphiques. Par ailleurs, meme si on utilise l'interruption DMA pour verifier qu'on est à l'envoi de l'octet 1, le temps de faire le traitement de la broche D/C, le DMA aura déjà démarré l'envoi de l'octet suivant et on aura un problème de synchronisation.

Mise en Oeuvre

La première chose, c'est le paramétrage du DMA

On doit, pour savoir quand le DMA a terminé, mettre en place un handler d'interruption de DMA en TX.
Cet Handler va vérifier qu'on attend bien la fin du transfert du dernier octet avant de couper le fonctionnement du DMA et ces interruptions.
C'est important car si le dernier octet n'est pas complètement envoyé, on risque de perdre des données.
L'interruption TX du DMA est appelé lors du dernier chargement d'octet dans le SPI.

void SPI_PORT_DMA_TX_IRQHandler() {

  // Test if DMA Stream Transfer Complete interrupt
  if (DMA_GetITStatus(SPI_PORT_TX_DMA_STREAM, DMA_IT_TCIF3)) {
    DMA_ClearITPendingBit(SPI_PORT_TX_DMA_STREAM, DMA_IT_TCIF3);
    /*
    * There is an unpleasant wait until we are certain the data has been sent.
    * The need for this has been verified by oscilloscope. The shift register
    * at this point may still be clocking out data and it is not safe to
    * release the chip select line until it has finished. It only costs half
    * a microsecond so better safe than sorry. Is it... 
    *
    * a) flushed from the transmit buffer
    */

    while (SPI_I2S_GetFlagStatus(SPI_PORT, SPI_I2S_FLAG_TXE) == RESET) {
    };

    /*
    * b) flushed out of the shift register
    */
    while (SPI_I2S_GetFlagStatus(SPI_PORT, SPI_I2S_FLAG_BSY) == SET) {
    };

    /*
    * the DMA stream is disabled in hardware at the end of the transfer
    * deselect both the displays here rather than try and work out
    * which one last got done
    */
    ILI9341_CS_SET;
    DMA_Flags |= DMA_FINISHED ;
    DMA_Cmd (SPI_PORT_TX_DMA_STREAM, DISABLE);
    DMA2_Stream3->CR &= ~DMA_SxCR_TCIE; // Disable TC int
  }
  DMA_ClearITPendingBit(SPI_PORT_TX_DMA_STREAM, DMA_IT_HTIF3);
  DMA_ClearITPendingBit(SPI_PORT_TX_DMA_STREAM, DMA_IT_FEIF3);
}



Puis on créée 2 fonctions pour le lancement du DMA: une pour démarrer le DMA et une pour faire la phase de lancement et d'attente de la fin du DMA

 

// start the DMA

void ILI9341_putBufferDMA (uint8_t *buf, uint32_t len)

{

  DMA_ClearITPendingBit(SPI_PORT_TX_DMA_STREAM, DMA_IT_TCIF3);

  DMA2_Stream3->CR |= DMA_SxCR_TCIE; // Enable TC int

  SPI_PORT_TX_DMA_STREAM->NDTR = (uint32_t) len;

  SPI_PORT_TX_DMA_STREAM->M0AR = (uint32_t) buf;

  DMA_Cmd (SPI_PORT_TX_DMA_STREAM, ENABLE);

}


// Init the ili9341 data transfer, start DMA and wait the end of DMA transfer

void ILI9341_SendData_DMA(uint8_t *buf, uint32_t len) {

    ILI9341_WRX_SET;

    ILI9341_CS_RESET;

    DMA_Flags &= (~DMA_FINISHED) ;

    ILI9341_putBufferDMA(buf, len);

    do {} while ((DMA_Flags & DMA_FINISHED ) == 0);

}


Il faut maintenant faire l'initialisation du DMA sur le bon canal en fonction de SPI utilisé: ici le SPI 1 et le canal DMA 3 :

      DMA_InitTypeDef DMA_InitStructure;

      // Set up the DMA

      // first enable the clock

      RCC_AHB1PeriphClockCmd(SPI_PORT_DMAx_CLK, ENABLE);

      // start with a blank DMA configuration just to be sure

      DMA_DeInit(SPI_PORT_TX_DMA_STREAM);

      // Configure DMA controller to manage TX DMA requests

      // first make sure we are using the default values

      DMA_StructInit(&DMA_InitStructure);

      // these are the only parameters that change from the defaults

      DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) & (SPI_PORT->DR);

      DMA_InitStructure.DMA_Channel = SPI_PORT_TX_DMA_CHANNEL;

      DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;

      DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

      // DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_byte ;

      DMA_InitStructure.DMA_Memory0BaseAddr = 0;

      DMA_InitStructure.DMA_BufferSize = 1;

      //DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte ;

      DMA_Init(SPI_PORT_TX_DMA_STREAM, &DMA_InitStructure);

      // Enable the DMA transfer complete interrupt

      DMA_ITConfig(SPI_PORT_TX_DMA_STREAM, DMA_IT_TC, ENABLE);



      NVIC_InitTypeDef NVIC_InitStructure;

      // enable the interrupt in the NVIC

      NVIC_InitStructure.NVIC_IRQChannel = SPI_PORT_DMA_TX_IRQn;

      NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

      NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

      NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

      NVIC_Init(&NVIC_InitStructure);

      // Enable dma tx request.

      SPI_I2S_DMACmd(SPI_PORT, SPI_I2S_DMAReq_Tx, ENABLE);


Bien sûr vous devez paramétrer votre SPI comme dans les exemples de Tilen ou dans mon code avant le DMA.

Voyons maintenant un usage (le code et les constantes sont soient visibles dans le code complet de Tilen, soit dans le mien)

// buffer for DMA transfer

#define MAX_DMA_BUFFER 2000

unsigned char  tabletmp[MAX_DMA_BUFFER + 2];

// function for filling screen


void TM_ILI9341_Fill(uint32_t color) {

    unsigned int n, i, j;

    // seperate the color word to 2 bytes

    i = color >> 8;

    j = color & 0xFF;

    // place Display RAM curseur on first byte

    TM_ILI9341_SetCursorPosition(0, 0, ILI9341_Opts.width - 1, ILI9341_Opts.height - 1);

    TM_ILI9341_SendCommand(ILI9341_GRAM);

    uint16_t len=0 ;

    // send color data in the DMA buffer

    for (n = 0; n < ILI9341_PIXEL; n++) {

        tabletmp[len++] = i;

        tabletmp[len++] = j;

        // if the MAX size fo the DMA is reach, then start DMA transfer

        if (len>=MAX_DMA_BUFFER) {

            // start the DMA transferand wait the end

            ILI9341_SendData_DMA(tabletmp,len);

            len=0;
        }
     }

     // if lenght "len" of color data is not to 0, so it needs to send the last bytes in a none full buffer

     if (len>0)
        ILI9341_SendData_DMA(tabletmp,len);

}



Voila globalement le fonctionnement du DMA et du SPI sur base de la librairie de Tilen. Bien entendu, chaque fonction de la librairie de base a été retouchée pour permettre l’intégration du DMA, avec quelques modifications mineurs.

Vous trouverez un code complet d'exemple ici : Il présente sur un projet simple sur base de STM32F401RE l'affichage d'une image et de texte.
Il utilise le DMA sur SPI 1 : ce n'est pas une librairie, mais un code d'exemple. si vous changez de SPI, vous devrez faire des adaptations





N’hésitez pas si vous avez des questions sur le fonctionnement