Acesso direto à memória (DMA) no Arduino Due 2

O Arduino Due foi o primeiro Arduino oficial a usar um microcontrolador fora da linha ATmega. O escolhido foi o AT91SAM3X8E,também da Atmel. Com um núcleo ARM Cortex M3 de 32 bits rodando a 84MHz, 96K de Ram e 512K de Flash, ele possui uma capacidade de processamento muito maior que o ATmega328 do Uno. Em termos de periféricos, ele também possui muitos recursos adicionais, como o DMA que veremos aqui. Infelizmente, o Due não alcançou muita popularidade, talvez por trabalhar a 3,3 V ao invés dos 5 V usados no Uno.

O DMA – Direct Memory Access – é um recurso que permite executar tarefas de movimentação de dados entre a memória e periféricos sem intervenção direta do processador. Para entender melhor, vejamos um exemplo, considerando um display ligado via I2C.

OLED com DMA

Material necessário

Para montar o nosso exemplo, vamos precisar de:

Como funciona o DMA

Na operação sem DMA (única forma disponível no Arduino UNO), o programa escreve os bytes a serem enviados ao display um a um no periférico de I2C. Antes de enviar um byte é preciso garantir que o byte anterior já foi enviado, o que é feito monitorando um registrador de status ou aguardando uma interrupção. O resultado é uma grande atividade do processador (se não usarmos interrupção ele ficará o tempo todo ocupado com esta atividade).

No Due, podemos programar o DMA para copiar os bytes de uma região da memória para o dispositivo de I2C. Uma vez feito o disparo, o processador só precisa intervir quando a transferência foi concluída. Durante a transferência em si o processador está livre. Isto pode fazer uma grande diferença quando desejamos atualizar continuamente o display todo. Sem DMA, o processador alterna entre gerar a próximo imagem e enviá-la ao display; com DMA a próxima imagem pode ser gerada enquanto a anterior está sendo enviada.

Um detalhe interessante é que o SAM3X possui vários DMAs. Vou usar aqui o Peripheral DMA (PDC) que é específico para uso com os periféricos seriais. A programação do PDC é mais simples e ele está integrado aos registradores dos periféricos seriais. O periférico que implementa o I2C é o TWI (Two Wire Interface).

Funcionamento DMA

Na figura acima podemos ver:

  • Os registradores TWI_RPR e TWI_RCR indicam o endereço e tamanho do buffer usado na recepção.
  • Os registradores TWI_TPR e TWI_TCR indicam o endereço e tamanho do buffer usado na transmissão.
  • Os status RX_READY e TX_READY (no periférico TWI) indicam para o PDC quando um byte foi recebido ou o TWI está pronto para transmitir um byte, Estes sinais são o que disparam a transferência de um byte entre o periférico e a memória.
  • Os status END_RX e END_TX no TWI são atualizados pelo PDC para indicar que todo o buffer foi recebido ou transmitido.

A biblioteca do Arduino não possui funções para manipulação do DMA, é necessário usar bibliotecas de mais baixo nível e acessar diretamente o hardware. A documentação básica para escrever o código é o datasheet do microcontrolador. A programação (sem DMA) do TWI pode ser vistas nos fontes da biblioteca do Arduino, que são instalados no seu micro quando você instala do suporte ao Arduino Due. São também instalados os fontes da Atmel com as definições das estruturas e rotinas de  baixo nível.

Programa para a comunicação usando DMA

Juntando tudo isso com um pouco de experimentação, cheguei ao exemplo abaixo:

/*
 * Teste da comunicação do Arduino Due com display I2C usando DMA
 * O display deve estar conectado a SDA/SCL (pinos 20 e 21) que correspondem a
 * WIRE_INTERFACE (Arduino) / TWI1 (Atmel)
 */

// Inclui o driver da Atmel
#include <include/twi.h>

// Endereço I2C do display
#define DISP_ADDR  0x3C

// Controle da comunicação com o display
typedef enum { DISP_INATIVO, DISP_INIT, DISP_CMDTELA, DISP_TELA } STATUS_DISP;
volatile STATUS_DISP stDisplay = DISP_INATIVO;

// Comandos do controlador do display
#define SSD1306_SETCONTRAST 0x81
#define SSD1306_DISPLAYALLON_RESUME 0xA4
#define SSD1306_DISPLAYALLON 0xA5
#define SSD1306_NORMALDISPLAY 0xA6
#define SSD1306_INVERTDISPLAY 0xA7
#define SSD1306_DISPLAYOFF 0xAE
#define SSD1306_DISPLAYON 0xAF
#define SSD1306_SETDISPLAYOFFSET 0xD3
#define SSD1306_SETCOMPINS 0xDA
#define SSD1306_SETVCOMDETECT 0xDB
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
#define SSD1306_SETPRECHARGE 0xD9
#define SSD1306_SETMULTIPLEX 0xA8
#define SSD1306_SETLOWCOLUMN 0x00
#define SSD1306_SETHIGHCOLUMN 0x10
#define SSD1306_SETSTARTLINE 0x40
#define SSD1306_MEMORYMODE 0x20
#define SSD1306_COLUMNADDR 0x21
#define SSD1306_PAGEADDR   0x22
#define SSD1306_COMSCANINC 0xC0
#define SSD1306_COMSCANDEC 0xC8
#define SSD1306_SEGREMAP 0xA0
#define SSD1306_CHARGEPUMP 0x8D
#define SSD1306_EXTERNALVCC 0x1
#define SSD1306_SWITCHCAPVCC 0x2

// Tamanho da tela
#define SSD1306_LCDWIDTH                  128
#define SSD1306_LCDHEIGHT                 64
#define TAM_TELA (SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8)

// Buffers para atualização da tela
uint8_t tela0[TAM_TELA+1];
uint8_t tela1[TAM_TELA+1];

// itela indica o buffer onde deve ser montada a próxima tela
uint8_t itela = 0;

// tabela de senos
uint8_t seno[SSD1306_LCDWIDTH];


// Iniciação
void setup() {
  // Inicia o display
  Twi_init();
  Display_init();

  // Pré-calcula senos
  for (int i = 0; i < SSD1306_LCDWIDTH; i++) {
    seno[i] = (uint8_t) ((sin(i/20.0)+1)*30.0);
  }
}

// Loop eterno
void loop() {
  // Descomentar o trecho abaixo para não desenhar em paralelo ao envio
  //while (!Twi_FimTfa()) {
  //}

  // Determina buffer a usar
  uint8_t *tela = (itela == 0) ? tela0 : tela1;
  tela++;

  // Apaga desenho anterior
  static int inicio = 0;
  for (int i = 0; i < SSD1306_LCDWIDTH; i++) {
    int y = seno[(i+inicio) % SSD1306_LCDWIDTH];
    int pos = (y/8)*SSD1306_LCDWIDTH + i;
    tela[pos] &= ~ (1 << (y % 8));
  }

  // Faz o novo desenho
  inicio = (inicio + 1) % SSD1306_LCDWIDTH;
  for (int i = 0; i < SSD1306_LCDWIDTH; i++) {
    int y = seno[(i+inicio) % SSD1306_LCDWIDTH];
    int pos = (y/8)*SSD1306_LCDWIDTH + i;
    tela[pos] |= 1 << (y % 8);
  }

  // Atualiza o display (espera acabar a apresentação da tela anterior se necessário)
  Display_refresh();
}

// Sequência de iniciação do display
uint8_t cmdInit[] =
{
    0,  // Co= 0, DC = 0
    SSD1306_DISPLAYOFF,
    SSD1306_SETDISPLAYCLOCKDIV, 0x80,
    SSD1306_SETMULTIPLEX, 0x3F,
    SSD1306_SETDISPLAYOFFSET, 0x00,
    SSD1306_SETSTARTLINE | 0x0,
    SSD1306_CHARGEPUMP, 0x14,
    SSD1306_MEMORYMODE, 0x00,
    SSD1306_SEGREMAP | 0x1,
    SSD1306_COMSCANDEC,
    SSD1306_SETCOMPINS, 0x12,
    SSD1306_SETCONTRAST, 0xCF,
    SSD1306_SETPRECHARGE, 0xF1,
    SSD1306_SETVCOMDETECT, 0x40,
    SSD1306_DISPLAYALLON_RESUME,
    SSD1306_NORMALDISPLAY
};

// Inicia a apresentação do display
uint8_t cmdStart[] =
{
    0,  // Co= 0, DC = 0
    SSD1306_DISPLAYON
};

// Sequência para atualização do display
uint8_t cmdTela[] =
{
  // Indica comando
  0,    // Co= 0, DC = 0
   
  // Define endereços iniciais e finais de colunas e páginas
  SSD1306_COLUMNADDR, 0, SSD1306_LCDWIDTH-1,
  SSD1306_PAGEADDR, 0, 7
};

// Iniciação do display
void Display_init() {
  Twi_send (DISP_ADDR, cmdInit, sizeof(cmdInit), DISP_INIT);
  delay(10);  // Precisa dar um tempo antes de iniciar a apresentação
  Twi_send (DISP_ADDR, cmdStart, sizeof(cmdStart), DISP_INIT);
  tela0[0] = tela1[0] = 0x40; // Co=0, DC = 1
  itela = 0;
  Display_clear();
}

// Limpa o display
void Display_clear() {
  uint8_t *tela;

  tela = (itela == 0) ? tela0 : tela1;
  memset (tela+1, 0, TAM_TELA);
  Display_refresh();
}

// Envia a tela atual para o display e chaveia para o outro buffer
void Display_refresh () {

  // copia o conteudo atual
  if (itela == 0) {
    memcpy (tela1, tela0, sizeof(tela0));
  } else {
    memcpy (tela0, tela1, sizeof(tela0));
  }

  // Garante que o refresh anterior terminou
  while (!Twi_FimTfa()) {
  }

  //chaveia a tela
  itela ^= 1;

  // dispara o envio do comando de atualização
  Twi_send (DISP_ADDR, cmdTela, sizeof(cmdTela), DISP_CMDTELA);
}

/* Parte de Baixo Nível */

Twi *twi = WIRE_INTERFACE;    // Lembrando que WIRE_INTERFACE = TWI1

static const uint32_t TWI_CLOCK = 400000; // Fast I2C, para normal usar 100000

// Iniciação do TWI1
void Twi_init() {
  // Desabilita o DMA
  twi->TWI_PTCR = TWI_PTCR_RXTDIS | TWI_PTCR_TXTDIS;

  // Habilita clock
  pmc_enable_periph_clk(WIRE_INTERFACE_ID);

  // Configura os pinos
  PIO_Configure(g_APinDescription[PIN_WIRE_SDA].pPort,
                g_APinDescription[PIN_WIRE_SDA].ulPinType,
                g_APinDescription[PIN_WIRE_SDA].ulPin,
                g_APinDescription[PIN_WIRE_SDA].ulPinConfiguration);
  PIO_Configure(g_APinDescription[PIN_WIRE_SCL].pPort,
                g_APinDescription[PIN_WIRE_SCL].ulPinType,
                g_APinDescription[PIN_WIRE_SCL].ulPin,
                g_APinDescription[PIN_WIRE_SCL].ulPinConfiguration);

  // Desabilita interrupção
  NVIC_DisableIRQ(WIRE_ISR_ID);

  // Configura TWI1 para mestre
  TWI_ConfigureMaster(twi, TWI_CLOCK, VARIANT_MCK);
}

// Dispara o envio de um buffer por DMA
void Twi_send (uint8_t addrSlave, 
  uint8_t *addrBuffer, 
  uint16_t tamBuffer, 
  STATUS_DISP novoStatus) {

  // Configura o endereço I2C para o qual será feita a transmissão
  twi->TWI_MMR = 0;
  twi->TWI_MMR = addrSlave << 16;

  // Configurar a transferência no DMA
  twi->TWI_TPR = (uint32_t) addrBuffer;
  twi->TWI_TCR = tamBuffer;

  // Interromper quando DMA finalizar de enviar os bytes (para enviarmos o stop)
  stDisplay = novoStatus;
  //TWI_EnableIt(twi, TWI_IER_ENDTX); // ERRO NO ASSERT
  twi->TWI_IER = TWI_IER_ENDTX | TWI_IER_TXCOMP;

  // Dispara a transferência
  NVIC_ClearPendingIRQ(WIRE_ISR_ID);
  NVIC_EnableIRQ(WIRE_ISR_ID);
  twi->TWI_PTCR = TWI_PTCR_TXTEN;
}

// Interrupção do TWI1 - enviar stop e disparar buffer seguinte se for o caso
void WIRE_ISR_HANDLER(void) {
  // Le o status
  uint32_t sr = TWI_GetStatus(twi);

  if (sr & TWI_SR_TXCOMP) {         // Terminou de enviar o STOP
    if (stDisplay == DISP_CMDTELA) {
      // Disparar o envio dos dados da tela
      uint8_t *tela = (itela == 0) ? tela1 : tela0;
      Twi_send (DISP_ADDR, tela, sizeof(tela0), DISP_TELA);
    } else {
      // Encerrado
      NVIC_DisableIRQ(WIRE_ISR_ID);
      stDisplay = DISP_INATIVO;
    }
  } else if (sr & TWI_SR_ENDTX) {  // Terminou de enviar os dados
    // Desliga o DMA
    twi->TWI_PTCR = TWI_PTCR_RXTDIS | TWI_PTCR_TXTDIS;
    
    // Envia Stop
    TWI_Stop(twi);
  }
}

// Verifica se acabou a transmissão (stop da ultima parte foi enviado)
uint8_t Twi_FimTfa() {
  return stDisplay == DISP_INATIVO;
}

Qualquer problema que você pode ter ao programar a placa Due, você pode checar o nosso post explicando como utilizá-la.

E aí, curtiu comunicar com o display usando DMA?  Você pode nos ajudar a melhorar o blog comentando abaixo.

Faça seu comentário

Acesse sua conta e participe

2 Comentários

  1. Very interesting tutorial! Do you have an example of how also use the DMA read of I2C interface?

    1. No, so far I’ve only used DMA for I2C and SPI writing.