Blue Pill - USART DMA

Table des matières

TL;DR

Utiliser le Direct Memory Access.

DMA

Grosso merdo (mais alors vraiment grosso et très certainement merdo, que les puristes me pardonnent), le DMA consiste à transférer le contenu d'une zone mémoire vers une autre sans l'aide du CPU. Je ne vais pas détailler le fonctionnement du DMA et de ses canaux mais pour faire court, une fois les composants configurés, l'activation du DMA va démarrer le transfert de données de la mémoire vers le registre de données de l'USART. Une fois terminé, une interruption est générée. Il me faut donc: On ne peut faire l'impasse sur le manuel de référence, page 282 pour obtenir les canaux utilisés (la Blue Pill ne dispose que d'un seul DMA) et leurs gestionnaires d'interruption associés:

Un peu de code

Je rajoute quelques lignes dans usart_config.h:
...
#elif defined USE_USART2
...
#ifdef USE_USART_TX_DMA
#define USART_TX_DMA_CHANNEL     DMA1_Channel7
#define USART_TX_DMA_IRQn        DMA1_Channel7_IRQn
#define USART_TX_DMA_IFCR_CTCIF  DMA_IFCR_CTCIF7
#endif
...
#ifdef USE_USART_TX_DMA
#ifndef USART_TX_DMA_BUF_SIZE
#define USART_TX_DMA_BUF_SIZE  128
#endif
#endif
Je ne fais pas le foufou avec la taille du tableau (USART_TX_DMA_BUF_SIZE), je rappelle que la mémoire est assez limitée et c'est bien assez pour du debug. Lors de l'initialisation de l'USART j'active le DMA et je configure le canal associé:
     1	#ifdef USE_USART_TX_DMA
     2	    __disable_irq();
     3	    USART_USART->CR3          |= USART_CR3_DMAT;                            // DMA enable Transmitter
     4	    RCC->AHBENR               |= RCC_AHBENR_DMA1EN;                         // DMA1 ENable
     5	    USART_TX_DMA_CHANNEL->CCR |= DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_TCIE; // Memory INCrement mode & data transfer DIRection (Read from memory) & Transfer Complete Interrupt Enable
     6	    NVIC_EnableIRQ(USART_TX_DMA_IRQn);
     7	    USART_TX_DMA_CHANNEL->CMAR = (uint32_t)usart_tx_dma_buf;                // set Channel Memory Address Register to TX buffer address
     8	    USART_TX_DMA_CHANNEL->CPAR = (uint32_t)&USART_USART->DR;                // set Channel Peripheral Address Register to usart Data Register address
     9	    __enable_irq();
    10	#endif
Sans DMA, on transmet les caractères au composant USART au travers de son registre Data Register:
void
usart_putc(char c) {
    while (!(USART_USART->SR & USART_SR_TXE)); // wait for Transmit data register Empty
    USART_USART->DR = c;
}
La version DMA place les caractères dans le tampon usart_tx_dma_buf (pas de gestion de débordement, c'est à l'appelant de faire attention):
void
usart_putc(char c) {
#ifdef USE_USART_TX_DMA
    usart_tx_dma_buf[usart_tx_dma_buf_len] = c;
    usart_tx_dma_buf_len++;
#else
    while (!(usart_usart->SR & USART_SR_TXE));
    usart_usart->DR = c;
#endif
}
Une fois le tampon rempli, on passe la main au DMA:
void
usart_printf(const char * const fmt, ...) {
...
#ifdef USE_USART_TX_DMA
    usart_tx_dma();
#endif
}
...
void
usart_puts(const char * const buf) {
...
#ifdef USE_USART_TX_DMA
    usart_tx_dma();
#endif
}
On renseigne la taille des données, on active le transfert et il ne reste plus qu'à attendre:
void
usart_tx_dma(void) {
    USART_TX_DMA_CHANNEL->CNDTR  = usart_tx_dma_buf_len; // set Channel Number of DaTa Register
    USART_TX_DMA_CHANNEL->CCR   |= DMA_CCR_EN;           // enable DMA Channel

    while (usart_tx_dma_buf_len); // wait for end of transfer
}
La fin du transfert génère une interruption et le gestionnaire remet à zéro le tableau:
...
#if defined USE_USART2
void
DMA1_Channel7_IRQHandler(void) {
    if (DMA1->ISR & DMA_ISR_TCIF7) {
        usart_tx_dma_reset();
    }
}
#elif defined USE_USART3
...

void
usart_tx_dma_reset() {
    USART_TX_DMA_CHANNEL->CCR &= ~DMA_CCR_EN; // disable DMA Channel
    usart_tx_dma_buf[0]        = '\0';        // reset buffer
    usart_tx_dma_buf_len       = 0;           // end of transfer
}
Le code complet: https://gitlab.com/dsx/blue-pill/-/tree/master/stm32f103c8t6/02_usart_usarts_dma

Ce qui me laisse un doute

J'ai maintenant 2 sources d'interruption (SYSTICK et DMA1) qui peuvent se déclencher en même temps mais comme SYSTICK est prioritaire, cela ne doit pas poser de problème.

A retenir

Utiliser le DMA économise du CPU au prix d'un peu de consommation mémoire (tableau) et de quelques lignes de code (configuration de l'USART et du canal DMA associé, gestionnaire d'interruption ...). Les options de l'USART sont désormais placées dans un fichier à part: Makefile.usart.

A suivre

J'en ai fini avec les USARTs, mais pas encore avec le debug et mon nouvel ami: SWO. Cet ami réclamant du matériel spécifique, on peut aussi s'occuper des conversions analogiques / numériques.