sexta-feira, fevereiro 26, 2016

Arduino Due, DMA e Fita de LED c/ APA102

Neste post vamos juntar o que vimos sobre a fita de LED com CI APA102 com o que estudamos sobre o Arduino DUE e experimentar o uso de DMA com o SPI.


O meu primeiro passo foi conferir que o meu programa anterior funcionava no Due. O único detalhe especial é que as conexões de SPI estão no antigo conector ISP:

Como o meu objetivo é melhorar o desempenho usando DMA, resolvi fazer um programa que informa quantas atualizações por segundo eu consigo em cada condição, para uma mesma "animação". Por simplificação, a minha animação consiste em acender uma cor de cada vez.

A primeira versão a testar é usando a biblioteca SPI padrão, sem DMA:
// Número de LEDs na fita  
const int NUM_LEDS = 60;
  
// Comandos para a fita  
byte fita1[4*(NUM_LEDS+2)];
byte fita2[4*(NUM_LEDS+2)];
  
// Teste 1 - Envio usando SPI.transfer (s/ DMA)
void teste1 (byte vel) {
  unsigned cont = 0;
  unsigned long termino;

  // iniciações
  SPI.begin();
  SPI.setClockDivider(84/vel);
  int i = 0;
  fita1 [i++] = 0;
  fita1 [i++] = 0;
  fita1 [i++] = 0;
  fita1 [i++] = 0;
  while (i < 4*(NUM_LEDS+1)) {
    fita1 [i++] = 0xE8;
    fita1 [i++] = 0xFF;
    fita1 [i++] = 0;
    fita1 [i++] = 0;
  }
  fita1 [i++] = 0xFF;
  fita1 [i++] = 0xFF;
  fita1 [i++] = 0xFF;
  fita1 [i++] = 0xFF;

  // Teste
  termino = millis() + 10000;
  while (termino > millis()) {
    // Envia imagem atual
    for (i = 0; i < 4*(NUM_LEDS+2); i++) {  
      SPI.transfer(fita1[i]);  
    }
    cont++;
    // Passa para a imagem seguinte
    for (i = 4; i < 4*(NUM_LEDS+1); i+=4) {
      if (fita1[i+1] == 0xFF) {
        fita1[i+1] = 0;
        fita1[i+2] = 0xFF;
      } else if (fita1[i+2] == 0xFF) {
        fita1[i+2] = 0;
        fita1[i+3] = 0xFF;
      } else {
        fita1[i+3] = 0;
        fita1[i+1] = 0xFF;
      }
    }
  }

  Serial.print ("Teste1 @");
  Serial.print (vel);
  Serial.print ("MHz: ");
  Serial.print (cont/10);
  Serial.println (" atualizacoes por segundo.");
}
O parâmetro vel é a velocidade em MHz. O padrão do Due é 4MHz (igual ao Uno). O Due trabalha com um clock do processador de 84MHz e o clock do SPI é este valor dividido por um número de 1 a 255.

A versão com DMA é bem mais complicada, pois vamos acessar diretamente o hardware. O que eu fiz aqui foi simplificar o código que eu achei em https://github.com/manitou48/DUEZoo/blob/master/dmaspi.ino.
//  Teste 2 - Acesso direto, c/ DMA
void teste2(byte vel) {
  unsigned cont = 0;
  unsigned long termino;

  // iniciações
  spiBegin();
  spiInit(84/vel);
  int i = 0;
  fita1 [i++] = 0;
  fita1 [i++] = 0;
  fita1 [i++] = 0;
  fita1 [i++] = 0;
  while (i < 4*(NUM_LEDS+1)) {
    fita1 [i++] = 0xE8;
    fita1 [i++] = 0xFF;
    fita1 [i++] = 0;
    fita1 [i++] = 0;
  }
  fita1 [i++] = 0xFF;
  fita1 [i++] = 0xFF;
  fita1 [i++] = 0xFF;
  fita1 [i++] = 0xFF;

  // Teste
  termino = millis() + 10000;
  while (termino > millis()) {
    // Envia imagem atual
    memcpy (fita2, fita1, sizeof(fita2));
    spiSend(fita2, sizeof(fita2));
    cont++;
    // Passa para a imagem seguinte
    for (i = 4; i < 4*(NUM_LEDS+1); i+=4) {
      if (fita1[i+1] == 0xFF) {
        fita1[i+1] = 0;
        fita1[i+2] = 0xFF;
      } else if (fita1[i+2] == 0xFF) {
        fita1[i+2] = 0;
        fita1[i+3] = 0xFF;
      } else {
        fita1[i+3] = 0;
        fita1[i+1] = 0xFF;
      }
    }
  }
  
  Serial.print ("Teste2 @");
  Serial.print (vel);
  Serial.print ("MHz: ");
  Serial.print (cont/10);
  Serial.println (" atualizacoes por segundo.");
}

// Rotinas para uso do SPI com DMA
// Adaptadas de https://github.com/manitou48/DUEZoo/blob/master/dmaspi.ino

/** chip select register number */
#define SPI_CHIP_SEL 3
/** DMAC transmit channel */
#define SPI_DMAC_TX_CH  0
/** DMAC Channel HW Interface Number for SPI TX. */
#define SPI_TX_IDX  1

/** Disable DMA Controller. */
static void dmac_disable() {
  DMAC->DMAC_EN &= (~DMAC_EN_ENABLE);
}
/** Enable DMA Controller. */
static void dmac_enable() {
  DMAC->DMAC_EN = DMAC_EN_ENABLE;
}
/** Disable DMA Channel. */
static void dmac_channel_disable(uint32_t ul_num) {
  DMAC->DMAC_CHDR = DMAC_CHDR_DIS0 << ul_num;
}
/** Enable DMA Channel. */
static void dmac_channel_enable(uint32_t ul_num) {
  DMAC->DMAC_CHER = DMAC_CHER_ENA0 << ul_num;
}

/** Poll for transfer complete. */
static bool dmac_channel_transfer_done(uint32_t ul_num) {
  return (DMAC->DMAC_CHSR & (DMAC_CHSR_ENA0 << ul_num)) ? false : true;
}

// start TX DMA
void spiDmaTX(const uint8_t* src, uint16_t count) {
  static uint8_t ff = 0XFF;
  uint32_t src_incr = DMAC_CTRLB_SRC_INCR_INCREMENTING;
  if (!src) {
    src = &ff;
    src_incr = DMAC_CTRLB_SRC_INCR_FIXED;
  }
  dmac_channel_disable(SPI_DMAC_TX_CH);
  DMAC->DMAC_CH_NUM[SPI_DMAC_TX_CH].DMAC_SADDR = (uint32_t)src;
  DMAC->DMAC_CH_NUM[SPI_DMAC_TX_CH].DMAC_DADDR = (uint32_t)&SPI0->SPI_TDR;
  DMAC->DMAC_CH_NUM[SPI_DMAC_TX_CH].DMAC_DSCR =  0;
  DMAC->DMAC_CH_NUM[SPI_DMAC_TX_CH].DMAC_CTRLA = count |
    DMAC_CTRLA_SRC_WIDTH_BYTE | DMAC_CTRLA_DST_WIDTH_BYTE;

  DMAC->DMAC_CH_NUM[SPI_DMAC_TX_CH].DMAC_CTRLB =  DMAC_CTRLB_SRC_DSCR |
    DMAC_CTRLB_DST_DSCR | DMAC_CTRLB_FC_MEM2PER_DMA_FC |
    src_incr | DMAC_CTRLB_DST_INCR_FIXED;

  DMAC->DMAC_CH_NUM[SPI_DMAC_TX_CH].DMAC_CFG = DMAC_CFG_DST_PER(SPI_TX_IDX) |
      DMAC_CFG_DST_H2SEL | DMAC_CFG_SOD | DMAC_CFG_FIFOCFG_ALAP_CFG;

  dmac_channel_enable(SPI_DMAC_TX_CH);
}

// Prepara para o uso da interface SPI
static void spiBegin() {
  PIO_Configure(
      g_APinDescription[PIN_SPI_MOSI].pPort,
      g_APinDescription[PIN_SPI_MOSI].ulPinType,
      g_APinDescription[PIN_SPI_MOSI].ulPin,
      g_APinDescription[PIN_SPI_MOSI].ulPinConfiguration);
  PIO_Configure(
      g_APinDescription[PIN_SPI_MISO].pPort,
      g_APinDescription[PIN_SPI_MISO].ulPinType,
      g_APinDescription[PIN_SPI_MISO].ulPin,
      g_APinDescription[PIN_SPI_MISO].ulPinConfiguration);
  PIO_Configure(
      g_APinDescription[PIN_SPI_SCK].pPort,
      g_APinDescription[PIN_SPI_SCK].ulPinType,
      g_APinDescription[PIN_SPI_SCK].ulPin,
      g_APinDescription[PIN_SPI_SCK].ulPinConfiguration);
  pmc_enable_periph_clk(ID_SPI0);
  pmc_enable_periph_clk(ID_DMAC);
  dmac_disable();
  DMAC->DMAC_GCFG = DMAC_GCFG_ARB_CFG_FIXED;
  dmac_enable();
}

//  configura a interface SPI
//  velocidade = 84/spiRate MHz
static void spiInit(uint8_t spiRate) {
  Spi* pSpi = SPI0;
  uint8_t scbr = 255;
  if (spiRate > 0) {
    scbr = spiRate;
  }
  //  disable SPI
  pSpi->SPI_CR = SPI_CR_SPIDIS;
  // reset SPI
  pSpi->SPI_CR = SPI_CR_SWRST;
  // no mode fault detection, set master mode
  pSpi->SPI_MR = SPI_PCS(SPI_CHIP_SEL) | SPI_MR_MODFDIS | SPI_MR_MSTR;
  // mode 0, 8-bit,
  pSpi->SPI_CSR[SPI_CHIP_SEL] = SPI_CSR_SCBR(scbr) | SPI_CSR_NCPHA;
  // enable SPI
  pSpi->SPI_CR |= SPI_CR_SPIEN;
}

// Aguarda fim do envio anterior e dispara novo envio
static void spiSend(const uint8_t* buf, size_t len) {
  Spi* pSpi = SPI0;
  // wait end of previous dma transfer
  while (!dmac_channel_transfer_done(SPI_DMAC_TX_CH)) {
  }
  // wait last byte sent
  while ((pSpi->SPI_SR & SPI_SR_TXEMPTY) == 0) {
  }
  // leave RDR empty
  uint8_t b = pSpi->SPI_RDR;
  // start new transfer
  spiDmaTX(buf, len);
}
O programa principal exercita as duas versões a 4, 7, 14, 21 e 28MHz. O programa completo pode ser baixado dos arquivos do blog (FitaLED_Due.ino). Os resultados estão no gráfico abaixo:


Na versão sem DMA a velocidade pouco mais que dobra quando passamos de 4 para 28MHz. Isto ocorre porque não temos paralelismo entre o envio e a geração da nova imagem e temos um trabalho significativo para disponibilizar cada byte a ser enviado.

Na versão com DMA a velocidade aumenta 7 vezes passando de 4 para 28MHz.O hardware cuida de mover os bytes da memória para o transmissor, liberando o processador para gerar a imagem.

25/03/16: Corrigido erro na versão com DMA e atualizado o gráfico e o texto com os novos resultados.

Nenhum comentário: