Go Down

Topic: I2C + DMA (Read 4752 times) previous topic - next topic

polpla

Hi,

I have been trying, unsuccessfully, to create an example where I can read an I2C interface to a sensor with DMA on the Arduino Due for the whole day. The documentation I could find of DMA functionality in the Arduino Due is limited to examples of SPI - which help a bit in understanding the general workings of DMA but I cannot manage to port to I2C. I also cannot find a proper reference for the low level Arduino functions involving DMA; which makes my live really hard. Any pointers to more complete examples or good references would be appreciated.

What I gathered from the SAM3x series datasheet is that the chip in the Arduino Due is capable of having 6 DMA channels and each of these can be configured to implement a series of protocols in their different hardware interfaces. In the case of SPI, for example, you want to use hardware interfaces 1 and 2 for SPI0 and 5 and 6 for SPI1. In the case of I2C the datasheet designates hardware interfaces 7 (RX) and 8 (TX) for TWI0.

With that I started trying to sketch out some code but I had still too many questions to produce something close to making sense... Anyway, if someone can shed some light here I would appreciate it immensely. Needless to say, I'll report on any additional progress I make and, if I manage to get it to work, I'll post it here.

Thanks!

mantoui

I'll be interested in your progress.  All I can offer  are SPI+DMA sketches based on SdFat stuff and a memory-to-memory DMA sketch, see

https://github.com/manitou48/DUEZoo

(I am actually messing with DMA+I2C on maple ... not much progress)

stimmer

It sounds like you're trying to use the AHB DMA controller. Before you go any further, check if the I2C hardware is supported by the PDC DMA controller - if it is, your life will be much easier. The AHB DMA controller is extremely fast (I got 28 million bytes/sec from it reliably) but very tricky to program. The PDC DMA controller isn't so fast but is far easier to program - just enable it, write the address of the data, then write the length and wait until it's done (or use an interrupt).

Unfortunately I don't know anything about I2C so can't help with that but can help with the DMA controllers.
Due VGA library - http://arduino.cc/forum/index.php/topic,150517.0.html

polpla

Hi stimmer,

Would you mind sending me a pointer to sample code that uses AHB DMA and PDC DMA?

Thanks!

stimmer

Here is a PDC example for the ADC converter, pin A0 (which is SAM3X channel 7) - the 3 sections marked *** are the important bits
Code: [Select]


uint16_t buf[512];


void setup(){
 Serial.begin(115200);
 
 // ADC hardware setup - ADC_FREQ_MIN is about 50ksps
 pmc_enable_periph_clk(ID_ADC);
 adc_init(ADC, SystemCoreClock, ADC_FREQ_MIN, ADC_STARTUP_FAST);
 ADC->ADC_MR |=0x80;
 ADC->ADC_CHER=0x80;
 ADC->ADC_CR=2;

 // *** This enables PDC DMA RX
 ADC->ADC_PTCR=1;  
}


void loop(){

 int q=micros();
   
   
 // *** PDC DMA start
 ADC->ADC_RPR=(uint32_t)buf; // address of buffer
 ADC->ADC_RCR=512;           // 512 samples - writing this starts DMA

 
 // do whatever you like here - DMA happens automatically
 Serial.println("DMA in progress");
 digitalWrite(13,random(2));
 delay(7);
 

 // *** wait for DMA to finish
 while(!(ADC->ADC_ISR & (1<<27)));

 
 q=micros()-q; // time how long DMA took
 
 int t=0;
 for(int i=0;i<512;i++)t+=buf[i];
 Serial.print("Total: ");
 Serial.println(t);
 Serial.print(q);
 Serial.println(" uS for 512 samples");
}


For AHB DMA all I know is the SPI examples which you have already seen, the link mantoui posted, and SdFat, and my VGA library - unfortunately the VGA library code is too hard to give as an example as it is impossible to follow, the registers all get set at different times in nested interrupts  :smiley-mr-green: I'd recommend the SPI example in mantoui's link, the code is derived from SdFat.
Due VGA library - http://arduino.cc/forum/index.php/topic,150517.0.html

raalst

I did a low-level bit of code to check out the DAC(s).
the second iteration was with the PDC.
maybe it contains something useful for you.

the code can be found as attachment at http://arduino.cc/forum/index.php/topic,151513.msg1141119.html#msg1141119

polpla

Ok, I think I got it to work...

The code below transmits in parallel to a code that prints a long string. When it finishes transmitting the port fires an interrupt breaking the printing execution from the main thread and printing the messages from the interrupt routine in the middle. Maybe I am overlooking something but I believe that means that the two things are running in parallel. Next step, handling back an forth communication.

Feedback is welcome.

Code: [Select]

// Speed of the twi clock
#define TWI_CLOCK 100000
// size of the buffer to transmit
#define SIZE 32

// definition of the buffer
uint8_t buf[SIZE];
// address of the device we are trying to communicate with
uint8_t txAddress = 0x18;

// Two wire interface instance (for reference look at component_twi.h
// found in the arduino folder/Resources/Java/hardware/arduino/sam/system/
// CMSIS/Device/ATMEL/sam3xa/include/component/
Twi* twi; 

void setup() {
 
  // initializing serial for debug
  Serial.begin(115200);
  Serial.println("transfering...");
 
  // filling the buffer with dummy data
  for(uint8_t i=0; i<SIZE; i++){
    buf[i] = i;
  }

  // function call to initialize the TWI (see details below)
  initWire(0);
  // configuring the two wire interface for master (that means arduino
  // talks, device listens)
  TWI_ConfigureMaster(twi, TWI_CLOCK, VARIANT_MCK);

  // write data procedure (details below)
  writeData();
  // we print something to kill some time (and hopefully get interrupted)
  Serial.println("1234567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=");
  delay(2000);
}

void loop() {
  // repeat until boredom
  writeData();
  Serial.println("1234567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=");
  delay(2000);
}

// function to initialize the two wire interface. I borrowed most of the code
// here from the Wire Library (Wire.h). Found in the arduino folder/Resources/
// Java/hardware/arduino/sam/libraries/Wire/Wire.h
void initWire(int i){
  switch(i){
  case 0: // TWI1 = SDA0, SCL0 on the Arduino board (sorry, I know this is confusing)
    pmc_enable_periph_clk(WIRE_INTERFACE_ID);
    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);
    NVIC_DisableIRQ(TWI1_IRQn);
    NVIC_ClearPendingIRQ(TWI1_IRQn);
    NVIC_SetPriority(TWI1_IRQn, 0);
    NVIC_EnableIRQ(TWI1_IRQn);
   
    twi = WIRE_INTERFACE;
    break;
  case 1: // TWI0 = SDA1, SCL1 on the Arduino board (sorry, I know this is confusing)
    pmc_enable_periph_clk(WIRE1_INTERFACE_ID);
    PIO_Configure(g_APinDescription[PIN_WIRE1_SDA].pPort,
                  g_APinDescription[PIN_WIRE1_SDA].ulPinType,
                  g_APinDescription[PIN_WIRE1_SDA].ulPin,
                  g_APinDescription[PIN_WIRE1_SDA].ulPinConfiguration);
    PIO_Configure(g_APinDescription[PIN_WIRE1_SCL].pPort,
                  g_APinDescription[PIN_WIRE1_SCL].ulPinType,
                  g_APinDescription[PIN_WIRE1_SCL].ulPin,
                  g_APinDescription[PIN_WIRE1_SCL].ulPinConfiguration);
    NVIC_DisableIRQ(TWI0_IRQn);
    NVIC_ClearPendingIRQ(TWI0_IRQn);
    NVIC_SetPriority(TWI0_IRQn, 0);
    NVIC_EnableIRQ(TWI0_IRQn);
   
    twi = WIRE1_INTERFACE;
    break;
  }
}

// simple function to send data and set the event for when that has finished sending.
void writeData(){
  // StartWrite sends the data. More info on the TWI functions can be found in twi.h
  // path: Arduino folder/Resources/Java/hardware/arduino/sam/system/libsam/include/twi.h
  // parameters:
  // 1=twi interfaces to send from/ 2=address of the device we are trying to reach/
  // 3=internal address (of the arduino due)/ 4=size of the internal address/ 5=first byte
  // of the buffer.
  TWI_StartWrite(twi, txAddress, 0, 0, buf[0]);
  // EnableIt enables the interrupts on the TWI interface. TWI_IER_TXCOMP is the event
  // for when the sending is complete. This will call TWI0_Handler or TWI1_Handler depending
  // which one we specify in the first parameter.
  TWI_EnableIt(twi, TWI_IER_TXCOMP);
}

// Handler for the interrups on the TWI1 interface (SDA and SCL pins in Arduino Due)
// prints the status of the SR register (potentially you could detect errors, etc.)
// for reference on the TWI_SR look at the component_twi.h (path above)
void TWI1_Handler( ) {
  Serial.println("done (TWI1)");
  Serial.print("SR:  "); Serial.println(twi->TWI_SR);
 
  // disables the event until the next transfer
  TWI_DisableIt(twi, TWI_IER_TXCOMP);
}

// Handler for the interrupts on the TWI1 interface (SDA and SCL pins in Arduino Due)
// prints the status of the SR register (potentially you could detect errors, etc.)
// for reference on the TWI_SR look at the component_twi.h (path above)
void TWI0_Handler( ) {
  Serial.println("done (TWI0)");
  Serial.print("SR:  "); Serial.println(twi->TWI_SR);
 
  // disables the event until the next transfer
  TWI_DisableIt(twi, TWI_IER_TXCOMP);
}

mantoui

#7
Mar 09, 2013, 07:44 pm Last Edit: Mar 09, 2013, 07:51 pm by mantoui Reason: 1
Are you really using DMA?  I don't see any reference to PDC. Is your test just allowing the I2C lib to run in the background, with its interrupt handler transmitting each byte??

 I just hacked a version of i2c for the Maple to use DMA -- no significant speed advantages, but presumably the CPU could do something else while the I2C transfer is ongoing.  DMA for the I2C write was straightforward, but on the maple , DMA for the I2C read was a bit more tricky (at least for me), having DMA receive the first n-2 bytes, and then letting the I2C driver manage the last two bytes so it could handle NACK and start/stop.  I only tested with an I2C EEPROM.

polpla

To be honest, I am not sure if this code is really using PDC since I couldn't find a proper reference for it. For what I gathered from the datasheet, It is my belive that on the Sam3x you have to actually disable PDC or TWI uses it by default. In the official Wire library for the Due you find this line:

Code: [Select]
// Disable PDC channel
twi->TWI_PTCR = UART_PTCR_RXTDIS | UART_PTCR_TXTDIS;


I imagine they put it there because otherwise the library would not be compatible with the Wire library for Arduino that other Arduino models run. By removing it I believe the code now uses PDC (although I am not 100% sure). The only thing that reinforces my theory is that the println call in my execution gets broken by the interrupt println.

I get this output:
123done (TWI1)
4567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=

Let me know what you think...

polpla

BTW, I just realized that the Wire code uses UART constants instead of TWI constants. They have the same values though...

Also, mantoui, would you mind sharing the i2c code you did for the Maple for reference? It might help as I continue to struggle with this...

Thanks!

mantoui

I don't think DMA is being utilized in DUE TWI functions. As noted in the ADC PDC example in this thread, somewhere there needs to be code that is setting the PDC address and byte-count registers for the DMA transfer.  I think your interrupted print, only shows that a TWI interrupt occurred after a byte transfer.  DMA can send many bytes, requiring only one interrupt when all of the bytes have been transferred.

Though the Maple and DUE share the ARM CPU architecture, the peripheral architecture (I2C, DMA, etc) are radically different.  So my maple I2C DMA code would not be very enlightening.  Eventually, I may make a github page for some of my maple hacks...

#11
May 01, 2013, 09:11 pm Last Edit: May 02, 2013, 02:08 pm by Sebastian Vik Reason: 1
EDIT2: Warning: this code does not correctly set the stop condition. It should be set prior to the last byte being transferred. For an N-byte transfer, we can transfer N-1 bytes by PDC, then interrupt, set the STOP flag and then send the last byte, also by PDC.
I've edited this code to always set the stop condition before activating the PDC, which works for 1-byte transfers. You can put it together or I might post my library when it's finished :)
/EDIT

I've got I2C working with PDC. It wasn't easy and the code is not pretty (yet) but it should get you started!

I'm writing an event-driven architecture where multiple drivers can communicate with one device each on the same I2C bus. There's some work left but when I'm done I should be able to run multiple I2C devices with nearly zero CPU overhead :)

Code: [Select]
#include <Arduino.h>
#define DeviceID                0x34
#define DeviceAddress           0x68
#define MPU6050_RA_WHO_AM_I     0x75
#define MPU6050_WHO_AM_I_BIT    6
#define MPU6050_WHO_AM_I_LENGTH 6
#define TWI_CLOCK               100000
uint8_t count = 0;
bool didWeRead;
enum ServiceState : uint8_t {
       FinishedReading,
       FinishedWriting,
       FinishedWaiting,
};

static inline void TWI_PDCWrite(uint8_t *data, uint16_t count) {
       WIRE_INTERFACE->TWI_PTCR = UART_PTCR_RXTDIS | UART_PTCR_TXTDIS;
       WIRE_INTERFACE->TWI_TPR = (RwReg)data;
       WIRE_INTERFACE->TWI_TCR = count;
       WIRE_INTERFACE->TWI_TNPR = 0;
       WIRE_INTERFACE->TWI_TNCR = 0;
}
static inline void TWI_PDCRead(uint8_t *data, uint16_t count) {
       WIRE_INTERFACE->TWI_PTCR = UART_PTCR_RXTDIS | UART_PTCR_TXTDIS;
       WIRE_INTERFACE->TWI_RPR = (RwReg)data;
       WIRE_INTERFACE->TWI_RCR = count;
       WIRE_INTERFACE->TWI_RNPR = 0;
       WIRE_INTERFACE->TWI_RNCR = 0;
}

static inline void TWI_MasterModeWrite(uint8_t deviceAddress) {
        WIRE_INTERFACE->TWI_MMR = TWI_MMR_IADRSZ_NONE | TWI_MMR_DADR(deviceAddress);
        WIRE_INTERFACE->TWI_CR = TWI_CR_MSEN;
}
static inline void TWI_MasterModeRead(uint8_t deviceAddress) {
        WIRE_INTERFACE->TWI_MMR = TWI_MMR_IADRSZ_NONE | TWI_MMR_DADR(deviceAddress) | TWI_MMR_MREAD;
        WIRE_INTERFACE->TWI_CR = TWI_CR_MSEN;
}

static inline void TWI_Write() {
        didWeRead = false;
        WIRE_INTERFACE->TWI_CR = TWI_CR_START | TWI_CR_STOP;
        WIRE_INTERFACE->TWI_IER = TWI_IER_ENDTX;
        WIRE_INTERFACE->TWI_PTCR = TWI_PTCR_TXTEN;
}
static inline void TWI_Read() {
        didWeRead = true;
        WIRE_INTERFACE->TWI_CR = TWI_CR_START | TWI_CR_STOP;
        WIRE_INTERFACE->TWI_IER = TWI_IER_ENDRX;
        WIRE_INTERFACE->TWI_PTCR = TWI_PTCR_RXTEN;
}

void write() {
       TWI_PDCWrite(&count, 1);
       TWI_MasterModeWrite(DeviceAddress);
       TWI_Write();
       count++;
}
uint8_t received[8];
void read() {
       TWI_PDCRead(received, 8);
       TWI_MasterModeRead(DeviceAddress);
       TWI_Read();
}
void InitializeTWI() {
       pmc_enable_periph_clk(WIRE_INTERFACE_ID);
       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);

       NVIC_DisableIRQ(TWI1_IRQn);
       NVIC_ClearPendingIRQ(TWI1_IRQn);
       NVIC_SetPriority(TWI1_IRQn, 0);
       NVIC_EnableIRQ(TWI1_IRQn);

       // Disable PDC channel
       WIRE_INTERFACE->TWI_PTCR = UART_PTCR_RXTDIS | UART_PTCR_TXTDIS;

       TWI_ConfigureMaster(WIRE_INTERFACE, TWI_CLOCK, VARIANT_MCK);
}


void setup() {
       SerialUSB.begin(115200);
       Serial.begin(115200);
       InitializeTWI();
}

void loop() {
       write();
       delay(1000);
       //read();
       //delay(1000);
}

void TWI1_Handler() {
       int sr = WIRE_INTERFACE->TWI_SR;
       WIRE_INTERFACE->TWI_IDR = TWI_IDR_ENDTX | TWI_IDR_ENDRX;
       WIRE_INTERFACE->TWI_PTCR = TWI_PTCR_TXTDIS | TWI_PTCR_RXTDIS;
       WIRE_INTERFACE->TWI_CR = TWI_CR_STOP;
       if(didWeRead) {
               Serial.print("Read");
               for(int i = 0; i < 8; i++) {
                       Serial.print(" ");
                       Serial.print(received[i]);
               }
               Serial.println("");
       } else {
               long time = micros();
               while(!(WIRE_INTERFACE->TWI_SR & TWI_SR_TXRDY));
               time = micros() - time;
               Serial.print("Iterations: ");
               Serial.println(time);
               read();
       }
}


EDIT: I'm building with make, so perhaps there are some differences (like #include <Arduino.h>)

Added corrections to the code above, now setting the STOP flag prior to transferring data. This code works correct only for 1-byte transfers, see top of post.

mlundh

I am currently using the DMA to handle the TWI communication on my due. Unfortunately I am not using the Arduino IDE, I am using atmel studio and FreeRTOS. I am not sure how much work it would be to port the functionality to be easily used in Arduino IDE, but if someone is interested in doing so i can highly recommend taking a look at the FreeRTOS_PERIPHERAL_CONTROL example that is included in atmel software framework(ASF). Unfortunately I do not think i have the skill to do it myself, but I am happy to help with whatever i can.

Hii everybody...
after reading the above healthy discussion of the I2C and the DMA..i gathered some knowledge about the DMA and how it can be initialized. But i have a confusion, i am not able to use ADC with PDC on arduino Due. I want to convert and read the analog data through PDC with some analog value given in the A0 pin of Due.

i am confused at what the counters and pointers in the PDC, what is the sole purpose of this and how a code can be formalized using these. Where can i read the converted data from ADC in PDC

Thanks in advance.....

Go Up