DMA - Direct Memory Access
O DMA (Direct Memory Access, ou Acesso Direto à Memória) no microcontrolador STM32F411CEU6 é um periférico projetado para transferir dados em alta velocidade entre memórias e periféricos sem a intervenção da CPU.
Para entender o impacto disso, pense na CPU como um professor altamente qualificado. Sem o DMA, cada byte que chega de uma leitura de sensor (SPI/I2C) ou que precisa ir para uma porta serial (UART) precisa ser pego pela CPU e movido manualmente. Se você estiver transferindo um buffer de 1 KB, a CPU ficará "ocupada" executando milhares de instruções apenas para mover dados.
Com o DMA, a CPU apenas configura a origem, o destino e o tamanho do pacote, e diz: "Transfira isso para mim". Enquanto o DMA move os dados em segundo plano, a CPU fica 100% livre para processar algoritmos pesados (como filtragem digital ou lógica de controle) ou pode entrar em modo de baixo consumo (Sleep).
Arquitetura do DMA no STM32F411
O STM32F411CEU6 possui 2 controladores DMA (DMA1 e DMA2). Cada controlador possui:
- 8 Streams (Canais de fluxo): Cada stream é totalmente independente e pode ser configurado para gerenciar transferências de dados.
- 8 Canais por Stream: Funciona como um multiplexador. Embora um stream tenha 8 canais de periféricos conectados a ele, apenas um canal pode estar ativo por vez naquele stream.
Diferença Crítica entre DMA1 e DMA2 no STM32F411:
- DMA1: Está conectado ao barramento APB1 (periféricos de baixa velocidade). Ele não tem acesso à memória Flash interna. Portanto, o DMA1 não pode fazer transferências do tipo Memory-to-Memory (Memória para Memória). Ele faz apenas Peripheral-to-Memory e Memory-to-Peripheral.
- DMA2: Está conectado ao barramento APB2 (periféricos de alta velocidade) e tem acesso à matriz de barramentos que alcança a Flash e a SRAM. Por isso, apenas o DMA2 pode realizar transferências Memória para Memória (por exemplo, copiar rapidamente uma array da Flash para a RAM).
Modos de Transferência Suportados
O DMA do STM32F411 é extremamente flexível e opera em três direções principais:
- Peripheral-to-Memory (Periférico para Memória):
- Exemplo: O ADC (Conversor Analógico-Digital) termina uma leitura e o DMA salva o resultado diretamente em uma array na SRAM.
- Memory-to-Peripheral (Memória para Periférico):
- Exemplo: Você tem um buffer de texto na RAM e o envia para a UART para transmissão serial sem travar o código.
- Memory-to-Memory (Memória para Memória - Apenas DMA2):
- Exemplo: Copiar blocos de dados ou inicializar buffers grandes instantaneamente.
Principais Recursos Técnicos
- Modo Circular (Circular Mode): Ideal para buffers contínuos (como amostragem de áudio ou leitura constante de sensores). Quando o DMA chega ao fim do buffer de memória, ele reinicia automaticamente do topo, criando um Ring Buffer em hardware, sem precisar que a CPU reconfigure os ponteiros.
- Double Buffer Mode (Modo de Buffer Duplo): O hardware alterna automaticamente entre dois buffers de memória. Enquanto o DMA está preenchendo o "Buffer A" com dados do periférico, a CPU pode ler e processar o "Buffer B" com total segurança, sem risco de corrupção de dados (race conditions).
- Incremento Automático de Endereço: Você pode configurar o DMA para incrementar o ponteiro de memória a cada transferência (para preencher uma array) enquanto mantém o endereço do periférico estático (já que o registrador de dados do periférico, como
USART_DR, está sempre no mesmo endereço de memória mapeada). - Tamanho do Dado Configurável: O DMA pode mover dados em tamanhos de Byte (8 bits), Half-Word (16 bits) ou Word (32 bits) independentemente na origem e no destino. O próprio hardware faz o alinhamento se você transferir, por exemplo, de um periférico de 8 bits para uma memória de 16 bits.
- FIFO (First-In, First-Out): Cada stream possui um buffer FIFO de 4 words. Isso permite gerenciar variações de velocidade entre os barramentos e suportar transferências em modo Burst (rajadas de dados).
Interrupções do DMA
Para avisar a CPU sobre o status da transferência, o DMA gera três interrupções principais muito utilizadas no desenvolvimento com a HAL ou LL:
- Transfer Complete (TC): O bloco inteiro de dados foi transferido.
- Half Transfer (HT): Metade dos dados foi transferida. Isso é crucial no Modo Circular, pois avisa a CPU que ela já pode processar a primeira metade do buffer enquanto o DMA continua preenchendo a segunda metade.
- Transfer Error (TE): Ocorreu um erro de barramento ou configuração durante a transferência.
Exemplo Prático de Aplicação (Contexto HAL)
Se você estivesse configurando o ADC no STM32CubeIDE para ler múltiplos canais (como um joystick e um sensor de temperatura) usando o STM32F411CEU6, o fluxo do seu código seria:
uint32_t adc_buffer[2]; // Buffer na SRAM para armazenar as leituras
int main(void) {
// ... Inicializações de Clock, GPIO, ADC e DMA ...
// Inicia o ADC em modo DMA apontando para o nosso buffer, com tamanho 2
HAL_ADC_Start_DMA(&hadc1, adc_buffer, 2);
while (1) {
// A CPU está completamente livre aqui!
// Os valores em adc_buffer[0] e adc_buffer[1]
// são atualizados em background pelo DMA.
}
}
// Callback chamado automaticamente quando o DMA termina de mover os 2 valores
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
// Opcional: fazer algo assim que os dados novos chegarem
}
Em suma: DMA2_Stream0_IRQHandler() é a função de hardware de baixo nível (o ponto de entrada do vetor de interrupção), enquanto HAL_ADC_ConvCpltCallback() é uma função de alto nível fornecida pela ST (STMicroelectronics) para facilitar a vida do desenvolvedor.
1. DMA2_Stream0_IRQHandler() — O Nível do Hardware
Esta função é o Handler de Interrupção (Interrupt Service Routine - ISR) propriamente dito. No STM32F411, o ADC1 geralmente está mapeado para o DMA2, Stream 0, Canal 0 (ou Stream 4, Canal 0).
- Quem a chama? O próprio hardware do microcontrolador. Quando o periférico DMA termina de mover a quantidade de dados programada, ele levanta uma flag de hardware, interrompe a CPU e força o contador de programa () a pular para o endereço desta função (mapeada na tabela de vetores no arquivo
startup_stm32f411xe.s). - Onde ela fica? Geralmente gerada no arquivo
stm32f4xx_it.c. - O que ela faz por padrão? Se você usa o STM32CubeIDE, dentro dela haverá apenas uma chamada para a biblioteca da ST:
void DMA2_Stream0_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_adc1);
}
- Características: Executa no contexto de interrupção com altíssima prioridade. O código aqui dentro deve ser o mais rápido possível.
2. HAL_ADC_ConvCpltCallback() — O Nível da Abstração (HAL)
Esta é uma função de Callback (retorno de chamada). Ela faz parte da camada de software da HAL (Hardware Abstraction Layer).
- Quem a chama? A própria função genérica da ST (
HAL_DMA_IRQHandler), depois que ela tratou o hardware de baixo nível. - O que a HAL faz antes de chamar o seu Callback?
- Limpa os bits/flags de interrupção no registrador do DMA (para evitar que a interrupção fique travada em loop).
- Verifica se houve erros de transferência.
- Se tudo estiver correto, ela chama
HAL_ADC_ConvCpltCallback().
- Onde ela fica? Ela é declarada na biblioteca da ST como uma função
__weak(fraca). Isso significa que você pode reescrevê-la em qualquer lugar do seu código (como nomain.c), e o compilador usará a sua versão em vez da versão vazia da biblioteca.
O Fluxo de Execução
Para visualizar como as duas interagem, imagine o fluxo temporal quando o ADC termina de preencher o buffer via DMA:
- Hardware: O DMA termina de transferir o último dado do ADC O hardware gera a interrupção.
- Vetor de Interrupção: A CPU para o
main()e pula paraDMA2_Stream0_IRQHandler(). - Tratamento HAL: A função chama
HAL_DMA_IRQHandler(), que limpa os registradores de status do DMA. - Notificação do Usuário: A HAL identifica que o periférico de origem era o ADC e finalmente chama o seu
HAL_ADC_ConvCpltCallback(). - Retorno: O código sai do callback, sai do handler e a CPU volta a executar o
main()exatamente de onde parou.
Comparação Direta
| Característica | DMA2_Stream0_IRQHandler() | HAL_ADC_ConvCpltCallback() |
|---|---|---|
| Camada | Hardware / Baixo Nível. | Software (HAL) / Alto Nível. |
| Portabilidade | Presa ao hardware específico (se mudar de chip ou mudar o Stream do DMA, o nome da função muda). | Alta portabilidade (funciona em quase qualquer STM32 que use o ADC com HAL). |
| Gerenciamento de Registradores | Você precisa limpar manualmente as flags de interrupção (se não usar a HAL dentro dela). | A HAL já limpou todas as flags antes de entregar o controle para você. |
| Múltiplos Periféricos | Específica daquele Stream do DMA. | Se você tiver múltiplos ADCs, todos vão cair no mesmo callback, exigindo que você teste quem chamou (ex: if(hadc->Instance == ADC1)). |
Qual você deve usar?
Use sempre o HAL_ADC_ConvCpltCallback(). Ela foi feita justamente para isolar a sua aplicação dos detalhes de hardware do DMA. Se amanhã você precisar mudar o ADC do Stream 0 para o Stream 4 no CubeMX, o seu código dentro do Callback continuará funcionando perfeitamente sem alterar uma única linha, enquanto qualquer lógica colocada diretamente na IRQHandler seria perdida ou teria que ser movida de arquivo.