Architecture et Principes du Contrôleur DMA
Le DMA (Direct Memory Access) est un mécanisme matériel permettant des transferts de données à haute vitesse entre différentes entités (périphériques, mémoires) sans solliciter le processeur central (CPU). En déléguant ces opérations de copie au contrôleur DMA, le CPU est libéré pour exécuter des tâches de calcul plus complexes, optimisant ainsi les performances globales du système embarqué.
Typologie des Transferts
Le contrôleur DMA gère trois types fondamentaux de flux de données :
| Direction du Flux | Cas d'Usage Typique |
|---|---|
| Mémoire vers Mémoire (M2M) | Copie de blocs de données importants entre la SRAM et la Flash. |
| Mémoire vers Périphérique | Envoi d'un tampon de données vers le register de transmission (TDR) d'un USART. |
| Périphérique vers Mémoire | Stockage des données reçues du registre de réception (RDR) d'un ADC ou USART vers la RAM. |
Caractéristiques Matérielles sur STM32F103
- DMA1 : Dispose de 7 canaux indépendants.
- DMA2 : Dispose de 5 canaux indépendants.
Chaque canal est dédié à une requête périphérique spécifique. En cas de requêtes simultanées, l'arbitrage s'effectue d'abord par priorité logicielle (configurable sur 4 niveaux dans le registre DMA_CCRx), puis par priorité matérielle (le canal avec l'index le plus bas est prioritaire à niveau égal).
Modes de Fonctionnement et Adressage
- Mode Normal : Le transfert s'arrête après l'envoi du nombre de données spécifié.
- Mode Circulaire : Le registre de compteur de données est rechargé automatiquement à la fin du cycle, permettant un transfert continu.
- Incrémentation des Pointeurs : Les adresses source et destination peuvent être configurées pour s'incrémenter automatiquement après chaque transfert, ou rester fixes (uttile pour les registres de périphériques).
Cas Pratique 1 : Transfert Mémoire vers Mémoire (M2M)
Implémentation avec la Bibliothèque HAL
Après avoir configuré le canal DMA en mode M2M via STM32CubeMX, le transfert est initié par logiciel. Voici une approche utilisant le polling pour vérifier la fin du transfert.
// Définition des tampons
const uint32_t flash_source_data[16] = {
0x00000000, 0x11111111, 0x22222222, 0x33333333,
0x44444444, 0x55555555, 0x66666666, 0x77777777,
0x88888888, 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB,
0xCCCCCCCC, 0xDDDDDDDD, 0xEEEEEEEE, 0xFFFFFFFF
};
uint32_t sram_dest_data[16];
void Execute_M2M_Transfer(void) {
// Démarrage du transfert (16 mots de 32 bits)
HAL_DMA_Start(&hdma_m2m_channel, (uint32_t)flash_source_data, (uint32_t)sram_dest_data, 16);
// Attente active de la fin de transmission
while (__HAL_DMA_GET_FLAG(&hdma_m2m_channel, DMA_FLAG_TC1) == RESET) {
// Le CPU attend la fin du transfert matériel
}
// Nettoyage du drapeau de fin de transfert
__HAL_DMA_CLEAR_FLAG(&hdma_m2m_channel, DMA_FLAG_TC1);
}
Implémentation avec la Bibliothèque Standard (SPL)
Pour les projets utilisant l'ancienne bibliothèque de périphériques, la configuration manuelle de la structure DMA est requise.
void Configure_M2M_DMA(void) {
DMA_InitTypeDef dma_cfg;
// Activation de l'horloge du bus AHB pour le DMA1
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
dma_cfg.DMA_PeripheralBaseAddr = (uint32_t)flash_source_data;
dma_cfg.DMA_MemoryBaseAddr = (uint32_t)sram_dest_data;
dma_cfg.DMA_DIR = DMA_DIR_PeripheralSRC;
dma_cfg.DMA_BufferSize = 16;
dma_cfg.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
dma_cfg.DMA_MemoryInc = DMA_MemoryInc_Enable;
dma_cfg.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
dma_cfg.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
dma_cfg.DMA_Mode = DMA_Mode_Normal;
dma_cfg.DMA_Priority = DMA_Priority_Medium;
dma_cfg.DMA_M2M = DMA_M2M_Enable; // Activation spécifique du mode M2M
DMA_Init(DMA1_Channel1, &dma_cfg);
DMA_Cmd(DMA1_Channel1, ENABLE);
}
Cas Pratique 2 : Transmision UART via DMA (Mémoire vers Périphérique)
L'utilisation du DMA pour l'UART permet d'envoyer de grandes chaînes de caractères sans bloquer le CPU dans des boucles d'attente de registre vide.
Approche HAL
uint8_t uart_tx_payload[] = "Telemetry data sent via DMA\r\n";
void Transmit_UART_DMA(void) {
// Le CPU configure le DMA et rend la main immédiatement
HAL_UART_Transmit_DMA(&huart1, uart_tx_payload, sizeof(uart_tx_payload) - 1);
}
Approche SPL
#define USART1_DR_ADDRESS (USART1_BASE + 0x04)
uint8_t tx_buffer[16] = "0123456789ABCDEF";
void Init_UART_DMA_Tx(void) {
DMA_InitTypeDef dma_cfg;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
dma_cfg.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
dma_cfg.DMA_PeripheralBaseAddr = USART1_DR_ADDRESS;
dma_cfg.DMA_DIR = DMA_DIR_PeripheralDST;
dma_cfg.DMA_BufferSize = 16;
dma_cfg.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // Registre fixe
dma_cfg.DMA_MemoryInc = DMA_MemoryInc_Enable;
dma_cfg.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
dma_cfg.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
dma_cfg.DMA_Mode = DMA_Mode_Normal;
dma_cfg.DMA_Priority = DMA_Priority_Low;
dma_cfg.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &dma_cfg);
DMA_Cmd(DMA1_Channel4, ENABLE);
// Liaison de la requête DMA au périphérique USART1
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}
Cas Pratique 3 : Réception UART de Longueur Variable (Périphérique vers Mémoire)
Pour recevoir des trames de taille inconnue sans utiliser d'interruptions par caractère (qui surchargent le CPU), la méthode optimale consiste à combiner le DMA circulaire avec l'interruption IDLE de l'UART. L'IDLE se déclenche lorsque la ligne RX reste inactive pendant la durée d'une trame, signalant la fin du paquet.
Logique de Réception avec HAL
Le processus consiste à démarrer le DMA, attendre l'interruption IDLE, arrêter temporairement le DMA pour calculer la taille reçue, traiter les données, puis relancer le DMA.
#define RX_BUFFER_CAPACITY 128
uint8_t uart_rx_buffer[RX_BUFFER_CAPACITY];
volatile uint16_t processed_rx_length = 0;
void Init_UART_Reception(void) {
// Activation de l'interruption de ligne inactive (IDLE)
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// Démarrage de la réception DMA
HAL_UART_Receive_DMA(&huart1, uart_rx_buffer, RX_BUFFER_CAPACITY);
}
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
// Vérification du drapeau IDLE
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// Arrêt du DMA pour figer le compteur
HAL_UART_DMAStop(&huart1);
// Calcul du nombre d'octets reçus
uint32_t unread_bytes = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
processed_rx_length = RX_BUFFER_CAPACITY - unread_bytes;
// Traitement des données (ou signalisation via un flag pour le main)
// Exemple: Analyser la commande reçue
if (strstr((char*)uart_rx_buffer, "CMD_START") != NULL) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}
// Relancement du DMA pour la prochaine trame
HAL_UART_Receive_DMA(&huart1, uart_rx_buffer, RX_BUFFER_CAPACITY);
}
}
Logique de Réception avec SPL
uint8_t rx_payload[RX_BUFFER_CAPACITY];
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
// Séquence de nettoyage matériel du flag IDLE (Lecture SR puis DR)
volatile uint32_t temp_register;
temp_register = USART1->SR;
temp_register = USART1->DR;
(void)temp_register;
// Désactivation du canal DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
// Extraction de la longueur utile
uint16_t frame_length = RX_BUFFER_CAPACITY - DMA_GetCurrDataCounter(DMA1_Channel5);
// Traitement de rx_payload sur frame_length octets...
// Réinitialisation du compteur et redémarrage
DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUFFER_CAPACITY);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}