Hi everyone,
I have recently started looking into using some of the lower-level functionality of the Teensy 4.1/iMX RT 1060, including DMAs, register-level access to pins etc, and eventually landed here. Up until now, I have been only doing typical Arduino-like things, maybe with some simple ISR sprinkled around - much like people earlier in this thread - but it simply wasn't enough to achieve what I needed (pretty unoriginally, access camera sensor and stream the resulting video to a screen). Having a lot of experience with dealing with this sort of stuff in my day job, I knew pretty precisely what I wanted to do, I just needed to know "how" on this hardware.
As I was going through the reference, Paul's core library sources and KurtE's camera sources, I was writing most of my findings up, and I thought I'll post them here, to provide some entry level information for people who need to move from simple digitalWrites to more complex scenarios. Technically, it's all there in the reference manual (
https://www.pjrc.com/store/teensy41.html#tech) but the problem is that the manual is riddled with a lot of domain-specific lingo, and (at least for me, coming to uControllers for a different field) it required some time to actually connect some dots and understand what the authors meant by certain things. Also the sources are well documented and are a great reference, but they can be totally overwhelming, when all you initially see is just tons of cryptic #defines.
All this is not really a rocket science, but I believe that it might be useful as an introduction to actually reading the reference manual - going over a particular use case and extracting the information needed for that from the docs. And if you think any of it is incorrect (which is quite possible), just say and I'll correct it here.
Just for some background: I wanted to have a DMA transfer data from an external camera sensor, to a buffer in memory. DMA stands for "Direct Memory Access" and the idea is that the CPU is not involved in that transfer - so free to do other stuff. I wanted that because the data I get from the sensor are just raw 12bit values from the internal ADC converter. Every few scanlines, I would like an interrupt to happen, so I could demosaic that received data, perform gamma correction, format conversion and write it out to a frame buffer. A separate DMA would periodically transfer the data from a frame buffer (that would be double buffered) to a screen. But the heart of all this are DMA transfers, and I wanted to get a fairly deep understanding of how they are set up and what is needed for them to work. I wanted to get a setup where:
- I have an clock signal that's connected to one of the Teensy pins (I also wanted to see how fast that clock signal can actually be)
- at each rising clock edge, the data is presented to some number of other Teensy input pins
- DMA picks up that data and stores it in a memory buffer
- when that buffer is full, I get an interrupt and I can do something inside the handler
- I don't really care about what happens next, any other situations are more complex scenarios; when I know how to deal with the basic stuff I can build on it and get something more complex
First things first: the external pins of the chip are called "pads" in the documentation. The word "pin" is used for input and output pins of the internal modules within the chip. And then there are the actual pins on Teensy itself, which are yet another thing. The pins from Teensy are connected to certain pads on the chip - and the schematics shows which pin goes there. The pads have these names like AD_B1_10 etc. The chip itself contains a number of modules serving different purposes - there's the DMA unit, there's the GPIO unit for dealing with General Purpose IO, there are timers, units to help with sensors, ADCs and tons of other stuff. Very important thing is that there's TONS of multiplexers (MUX). Their sole purpose is to connect certain pins of different units together. They are programmable and allow for tons of flexibility - so if you want the input from certain pad to act as DMA source, you set up a bunch of MUXes and voila. You want to just read the input from that pad in your code, you set them up differently and it works. The problem is the sheer number of these multiplexers, as they are literally everywhere. And they are documented out of order, the diagrams are crap and it takes quite some time to actually decipher all that. Configuration of all these systems is done through writing to different memory-mapped registers. This means that the configuration state is simply visible as an address somewhere in memory - which you can read or write to. The address for each register described in the docs is provided somewhere around it - for instance on page 500, SW_MUX_CTL_PAD_GPIO_AD_B1_08 SW MUX Control Register (IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_08) is said to reside at the address: 401F_8000h base + 11Ch offset = 401F_811Ch. Since it's 32 bit register, you can do uint32_t* register_IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_08 = (uint32_t*)(0x401F811C); and simply access it through that pointer. Teensy core libraries however provide TONS of macros and helper structs for accessing these things, so you don't have to copy these numbers from the docs.
Let's start with the simpler thing first - getting the signal from the Teensy input pins to where it can be accessed. The Teensy pins mean the microcontroller pads, and these are first routed through the IOMUX controller (Chapter 11 in the reference manual). It allows to route the signal from the pads to different subsystems. This is quite interesting, as treating them as input-output pins (so GPIO) is only one option - the pads don't have to actually be used this way! They can be hooked up to ADC converters, some UARTS, FLEXIO module and tons of other stuff - but it's not important at the moment. The important bit is that there is this flexibility here and we need to set them up to be routed to GPIO, as this is something we can access later on.
The registers controlling where the input of each pad goes are described in Chapter 11.7. For instance, Teensy 4.1 pi number 23 is connected to pad AD_B1_09 (see here
https://www.pjrc.com/store/teensy41.html#tech) which is controlled by the register IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_09 described on page 501. You can select one out of the 10 modes for that pin, by writing the corresponding value to that register - writing 6 there will for example set it to be routed to USDHC2_CLK signal in usdhc2 module (which, I'm sure you can find details on in that manual too). We want our inputs to be set to the GPIO modes - which here would mean writing 5 to that register. Teensy core libraries actually do that by default (or at least I think they do, quickly skimming through the code it didn't pop up, but the configuration is usually done through macros, and there's quite a few of them, so I probably just missed it), so you don't really need to change it, if you just want your input signal as 0 or 1 somewhere. The code that simplifies all that setup is here
https://github.com/PaulStoffregen/cores/blob/master/teensy4/core_pins.h. The *really* important bit here is the name of the input pin in the GPIO unit specified in the register description - for that AD_B1_09 it's GPIO1_IO25. This means that that particular pad, AD_B1_09 gets connected to the GPIO1 to its 25th input. KurtE has actually a brilliant diagram coalescing all this data into a readable form, here:
https://github.com/KurtE/TeensyDocuments/blob/master/Teensy4.1 Pins.pdf. And another one, with all the MUX modes for every pin:
https://github.com/KurtE/TeensyDocuments/blob/master/Teensy4.1 Pins Mux.pdf - which shows where the signal from each Teensy pin can be routed to.
We have the signal from our input pins routed to GPIO unit by the IOMUX now. Next we need to look at th GPIO unit itself to see how we can access it. This is covered in Chapter 12, page 949. We can see that there are 9 GPIOs, first five of them being standard speed, working on the IPG_CLK clock (it's the peripheral clock, described in Chapter 14, it runs at 1/4th of a speed of the ARM core, so if you have the Teensy running at 600MHz, it runs at 150MHz), and the next four being "fast ones", runnig at the same speed as the core (so 600MHz) *but* sharing the same pins as the last four of the regular ones ("pins" as the input pins to this module, not the Teensy pins/pads - so we're talking about these signals called GPIO1_IO25). This means that a signal routed to GPIO1_IO25 can be read both through GPIO1 AND GPIO6. Of course, there's a MUX to pick which one we want to use. The switching is controlled *per pin*, via IOMUXC_GPR_GPR26-29 registers (pages 375+).
Each GPIO allows to access the data through, again, memory-mapped registers. All the pins of a particular GPIO appear as individual bits of these registers - so in a single, 32bit memory access you can read the content of the entire GPIO unit. For instance, if you want to read the entire GPIO1, you simply read 32 bit value from the address 0x401B8000 (or simply use the GPIO1_DR macro from the core libraries; the data registers for GPIO are described on page 962.). This is why the mapping from Teensy pins/pads to GPIO units (the one on KurtE diagram) is so important - because you want all your signals to be hooked up to Teensy pins that live on a single GPIO - so you can read them all in one go, instead of touching multiple memory locations and doing bit twiddling. The best GPIO for that is GPIO1 (GPIO6 in the fast version) that has 20 external Teensy pins that can be routed to it (and 16 of the being contiguous, so if you route your data in a particular order, you don't even have to mess if it much on the code side, just do a bit shift). Let's assume we want to use Teensy pins 14, 15, 40, 41, 17, 16, 22, 23, 20, 21. They correspond to outputs 18-27 of the GPIO1.
Another important bit here is the direction - the GPIO can be used both to read and to output external signals, and the mode in which each GPIO bit works is controlled by another register - the direction GPIOx_GDIR. We want to read our data, so we want to set them to inputs - which means clearing corresponding bits of the GDIR register:
Code:
GPIO1_GDIR &= ~(0x0FFC0000u);
Now the tricky bit is that the "fast" GPIO cannot be read by the DMA. This isn't stated anywhere explicitly, but Paul mentions it somewhere here on the forum. It sort-of makes sense, as the DMA is clocked by the same clock as the "regular" GPIO, while the "fast" ones are clocked much higher, but an explicit note in the docs would be nice. To make our inputs readable to the DMA, we need to set them to be "regular" (so hooked up to GPIO1, not GPIO6) with the corresponding IOMUXC_GPR_GPR26-29 register. To switch our input pins to the "regular" mode we need to clear these bits in IOMUXC_GPR_GPR26:
Code:
IOMUXC_GPR_GPR26 &= ~(0x0FFC0000u);
With such setup, we have our input Teensy pins prepared to route the signals to the DMA. Now comes the more tricky bits: setting the DMAs and the XBAR (ugh).
The DMA subsystems are described in Chapters 5 and 6, with some really useful information also in Chapter 4, but we'll get to that. Long story short, the purpose of the DMA is to move data around memory, without the CPU being involved. Usually, when the data is transmitted, we want some notification (like an interrupt), but we can just as well simply poll for the completion in a busy loop. There are technically 32 DMA channels on the chip, but this is something I consider a bit of a lie. They are not independent DMA channels that run in parallel, there's in fact an arbitration unit and priorietes and picking which channel should be serviced at a given moment - so in reality there's only one transaction being performed at a given time (well, through this general purpose DMA - there are other specialized DMA controllers in other units that in fact run in parallel).
The configuration of each channel is described but a Transfer Control Descriptor (TCD). It contains things like the source address, number of bytes to copy in each transfer, the destination address etc. That structure is precisly layed out on page 116, and individual fields are described on subsequent pages. They all live, as you might have guessed, in a global memory space - so again, to configure them, you need to write the desired things to certain places in memory.
DMA transfers happen in chunks, called minor loops. The minor loop is executed then the channel receives a transfer request - this is an important concept, and more on that later, but it's important to realize that it makes it pretty different from how DMAs behave in other systems. Again, for me, they've been traditionally fire-and-forget requests - I want some data copied from here to here, alternatively filled with some value or similar, don't bother me until you're done. Here it works very different. When you set up a channel and enable it, it doesn't generally transfer anything until it gets that request (it *can* just transfer the data without any request too, but all this is something you set up). And when it gets that request it executes a *single* minor loop, not everything (and, of course, you can totally set it up to transfer everything, but again, you don't have to and that's something you set up). This is way more flexible mechanism, but you need to be aware of that.
Each minor loop transfers some number of bytes (NBYTES field from the TCD). The bytes are fetched from the source address (SADDR field in the TCD) and after grabbing some number of them (the actual count is defined by the SSIZE field), an offset value is added to the source address (SOFF field in the TCD). Then some number bytes (defined by DSIZE field in the TCD) are written to the destination address (DADDR in the TCD) and the destination address is modified by its offset values (DOFF in the TCD). A TCD can define to execute such minor loop multiple times, which is called a major loop - via BITER and CITER fields. They define, respectively, beginning iteration count for the major loop and the current iteration count for the major loop (they should start with the same value generally). There's also a bunch of other fields - you can add some value to the addresses on completion of the major loop, you can do channel linking, when major (or minor) loop completion triggers another channel etc - but these are more complex cases, we're not really interested in right now.
The important bits right now are that, since the data from the input Teensy pins is actually available under certain address, we can read it with the DMA. For that, we set the source address to memory mapped GPIO1 data register GPIO1_DR, and we set the source offset to not change (SOFF = 0). On the destination side, we put the address of our buffer in the destination address, and we set it to add 4 bytes to it on every transfer. We set the transfer size to 4 bytes, since GPIO registers allow only 32bit access. Then we set the major loop count to the size of our destination buffer. This setup gives us exactly what we want: at every clock cycle we will generate a request (how exactly is explained below), and at each request we will grab a single, 4 byte piece of data presented at the input pins and write them to the destination buffer. Then, at another clock cycle we will get another four bytes and so on, until we fill the entire buffer, which corresponds to finishing the major loop.
Teensy core libraries provide a really nice wrapper for all this functionality - the DMAChannel class here:
https://github.com/PaulStoffregen/cores/blob/master/teensy4/DMAChannel.h, that lets you specify the interesting things without touching the actual registers. There is a couple of things to be careful about: first, the interface is constructed in a way that generally discouragues from explicitly defining the which hardware channel we'll actually be using, they are by default just allocated sequentially. You can technically override it, but it's a bit cumbersome. In situations like this, I'm much more used to specifying these sort of indices explicitly, so there are no surprises - especially that certain functionality is available only on certain channels (periodic triggering for instance). The other thing that was a bit of surprise was inferring the number of bytes to transfer in each transaction from the data type of the buffer passed. Again, might be just personal background, but passing the buffer in a type-agnostic way and explicitly providing the size of a single transfer would seem more natural.
All the setup boils down to:
Code:
dmachannel.begin();
dmachannel.source( GPIO1_DR );
dmachannel.destinationBuffer( dmaBuffer, DMABUFFER_SIZE * 4 );
so on every request, DMA will grab data from GPIO1, and write it to the destination buffer. We also wanted it to trigger interrupt when the entire buffer is full, and this is simply done with:
Code:
dmachannel.interruptAtCompletion();
dmachannel.attachInterrupt( dmaInterrupt );
The first call just sets a bit in the TCD, the other one writes the address of the interrupt handler to an interrupt handlers table.
We now need the final piece of the puzzle - a DMA request generated at every clock cycle on some Teensy input pin.
How the DMA requests work is described in Chapter 5, on DMAMUX. The purpose of this system is to route signals that generate DMA *requests*. There is a number of signals that can act as DMA requests, and each of them can be connected to any of the DMA channels - and DMAMUX is used to set up this connection. The confusing thing is that throughout the chapter the signals that act as DMA requests are referenced to as "sources", while they don't have anything to do with the source of the *data* for the transfer - so just keep that in mind when reading all this. To make it even more confusing, the first four DMA channels have *trigger* capability - but it's not "triggering" the transfer, but rather periodic triggering on top of the actual request - when you have some source of the DMA request, you can additionally configure a periodic timer, and only when both request source AND the periodic trigger are high, the DMA request is actually generated. Not using "triggers", just regular source, also "triggers"/generates the request, it just doesn't have this additional, periodic gating on top. This is described on pages 79-80, and is actually pretty logical, as long you remember that the "peripheral request" is the DMA request source signal, and the "trigger" is that period trigger that you set up somewhere else (in the Periodic Interval Timer system).
For our transfer we need a request on every clock cycle. The problem is that the DMA requests can only come from certain places. The full list is on page 52, and the GPIO is simply not there. So you cannot trigger DMA through the GPIO system, it needs to come from somewhere else. Closer inspection of the list reveals that there does contain XBAR, which can actually even act as source of four independent DMA request. XBAR is yet another multiplexer. That subsystem actually contains 3 different multiplexers, all pretty gigantic, with the first being able to connect 80-something inputs to 130-something output, but there's also XBAR2 and XBAR3 which can do additional connections (though for slightly different signals). The list of the available inputs and outputs for each of the XBARs is listed in chapter 4, on pages 61+.
If you look at the input list for the XBAR, among other things you'll see IOMUX_XBAR_xxx which are outputs from IOMUX, which, as we've already seen before are the multiplexers governing the input pads, so Teensy input pins. Before, we had them set up to route the inputs to GPIO, but we mentioned that we can also redirect them to other subsystems. Now we want to redirect the input from the pin that will get the clock signal to the XBAR. Looking at KurtE's diagram, we can see which pads/Teensy pins can be routed to XBAR, and which XBAR input they end up as. Picking Teensy pin 4, we can read that it can get routed to XBAR input 8.
The register controlling Teensy pin 4/pad EMC_06 is called IOMUXC_SW_MUX_CTL_PAD_GPIO_EMC_06 and is described on page 435. According to the table, we need to set its mode to 3 to route it to XBAR (again, the more comprehensive version of all these assignments is here:
https://github.com/KurtE/TeensyDocuments/blob/master/Teensy4.1 Pins Mux.pdf).
Code:
IOMUXC_SW_MUX_CTL_PAD_GPIO_EMC_06 = 3;
Then we connect the input 8 of the XBAR to the XBAR DMA request output. Scrolling the list of the XBAR1 outputs (page 68), we can see that output 0 corresponds to the XBAR DMA request 0 in the DMAMUX (input 30 in the DMA MUX on page 54 - this table is a bit weird again, as it lists the sources of DMA requests as "channels", but they are not DMA channels... ugh, confusing). Configuring XBAR connections is done through registers described in Chapter 61, on pages 3235+, but Teensy libraries provide a function called xbar_connect and a macros for all the XBAR inputs and outpus so it's all a bit more readable. All we need to do is call
Code:
xbar_connect( XBARA1_IN_IOMUX_XBAR_INOUT08, XBARA1_OUT_DMA_CH_MUX_REQ30 );
(the xbar_connect function is defined in pwm.c, so you might need to either link the file to your project, or copy that function over).
Now comes a bit of more obscure stuff, that I'm sure I would have missed in the docs, but KurtE sources do all this, so I had some reference point to look for. First, the XBAR1_INOUT8 is called "INOUT" for a reason - it can be both input and output. Since we want it to be input, we need to set it this way: IOMUXC_GPR_GPR6 register (page 344) sets a direction for a bunch of XBAR I/O pins. We want to clear the bit corresponding to INOUT8 to set it as input:
Code:
IOMUXC_GPR_GPR6 &= ~(IOMUXC_GPR_GPR6_IOMUXC_XBAR_DIR_SEL_8)
Then, certain XBAR inputs can be driven by two different input pads. This is called daisy-chaining, and for XBAR1_IN08 is controlled by register IOMUXC_XBAR1_IN08_SELECT_INPUT (page 906) - we can choose between it begin driven by pad EMC_06 (so Teensy pin 4) or by pad SD_B0_04 (one of the SDIO pins). We want to use Teensy pin 4, so we set it to 0
Code:
IOMUXC_XBAR1_IN08_SELECT_INPUT = 0;
We also want the XBAR to react to an edge of the input signal, not really the level, and we want to trigger the DMA on that edge. This is controlled by the XBARA1_CTRL0 register, see page 3271 for details. We want to enable edge detection on XBARA_OUTPUT00, detect rising edge, and generate a DMA request when it occurs:
Code:
XBARA1_CTRL0 = XBARA_CTRL_STS0 | XBARA_CTRL_EDGE0(1) | XBARA_CTRL_DEN0;
This is tbh, at least to me, a bit confusing, as the XBAR1_OUTPUT00 is XBAR1_DMA - so why do we additionally need to enable the XBARA_CTRL_DEN0 bit here? What would it actually mean to have something routed to that output but without this bit set? Seems like it wouldn't generate these request, but in that case what does this connection actually mean? I'm not really sure.
Anyway.... the only remaining thing is to actually set up our DMA channel to execute it's minor copy loop when triggered by the XBAR request:
Code:
dmachannel.triggerAtHardwareEvent( DMAMUX_SOURCE_XBAR1_0 );
And done!
Well, not quite, because, surprise, surprise, XBAR doesn't seem to be clocked by default, so it just doesn't work if you don't enable its clock explicitly (clock gating register described on page 1086):
Code:
CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON);
And you need to enable that clock *before* you start messing with XBAR settings, otherwise, they wont take any effect.
And now we're really really done. When we enable the DMA channel, it will wait for the clock signal on Teensy pin 4, and its each rising edge will trigger a minor DMA loop, copying 4 bytes from the GPIO1 to the output buffer. When the output buffer is full, it will trigger the interrupt. One useful thing to mention here, is that you need to clear the interrupt flag in the interrupt handler:
Code:
dmachannel.clearInterrupt();
asm("DSB");
The DSB instruction is a memory barrier, ensuring that the following code will not be executed before the previous memory operations complete (apparently it can be pretty common to finish the execution of the interrupt handler, before that clear actually getting through, due to differences in the clock rates between different systems)
Here's the complete code you can just build and try out. It uses the setup as described above, so you need a clock signal on pin 4. On the data pins I present a binary counter, which I then check for correctness - this is mainly to check how fast I can get this data from the input pins with the DMA ((it only does 8 bit data, instead of 10, too many cables to connect ;-). The disappointing bit is that it's not really that fast. 10MHz seem to work fine, but anything above is just generating errors - some values are missed. I'm not entirely sure if this is problem with the slowness of RT1060 or my setup - I have the signal lines connected with ~15cm cables, so maybe there's a problem with the signal integrity by the time it gets to the input pins. But I don't really have neither the equipment nor the experience to reliably measure a >10MHz signal. So maybe it's possible to get it to work slightly faster too - but I wouldn't expect anything crazy - the DMA is only clocked at 1/4th of the core frequency.
Code:
#include <DMAChannel.h>
DMAChannel dmachannel;
#define DMABUFFER_SIZE 4096
uint32_t dmaBuffer[DMABUFFER_SIZE];
int counter = 0;
unsigned long prevTime;
unsigned long currTime;
bool error = false;
bool dmaDone = false;
uint32_t errA, errB, errorIndex;
// copied from pwm.c
void xbar_connect(unsigned int input, unsigned int output)
{
if (input >= 88) return;
if (output >= 132) return;
volatile uint16_t *xbar = &XBARA1_SEL0 + (output / 2);
uint16_t val = *xbar;
if (!(output & 1)) {
val = (val & 0xFF00) | input;
} else {
val = (val & 0x00FF) | (input << 8);
}
*xbar = val;
}
void dmaInterrupt()
{
dmachannel.clearInterrupt(); // tell system we processed it.
asm("DSB"); // this is a memory barrier
prevTime = currTime;
currTime = micros();
error = false;
uint32_t prev = ( dmaBuffer[0] >> 18 ) & 0xFF;
for( int i=1; i<4096; ++i )
{
uint32_t curr = ( dmaBuffer[i] >> 18 ) & 0xFF;
if ( ( curr != prev + 1 ) && ( curr != 0 ) )
{
error = true;
errorIndex = i;
errA = prev;
errB = curr;
break;
}
prev = curr;
}
dmaDone = true;
}
void kickOffDMA()
{
prevTime = micros();
currTime = prevTime;
dmachannel.enable();
}
void setup()
{
Serial.begin(115200);
// set the GPIO1 pins to input
GPIO1_GDIR &= ~(0x03FC0000u);
// Need to switch the IO pins back to GPI1 from GPIO6
IOMUXC_GPR_GPR26 &= ~(0x03FC0000u);
// configure DMA channels
dmachannel.begin();
dmachannel.source( GPIO1_DR );
dmachannel.destinationBuffer( dmaBuffer, DMABUFFER_SIZE * 4 );
dmachannel.interruptAtCompletion();
dmachannel.attachInterrupt( dmaInterrupt );
// clock XBAR - apparently not on by default!
CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON);
// set the IOMUX mode to 3, to route it to XBAR
IOMUXC_SW_MUX_CTL_PAD_GPIO_EMC_06 = 3;
// set XBAR1_IO008 to INPUT
IOMUXC_GPR_GPR6 &= ~(IOMUXC_GPR_GPR6_IOMUXC_XBAR_DIR_SEL_8); // Make sure it is input mode
// daisy chaining - select between EMC06 and SD_B0_04
IOMUXC_XBAR1_IN08_SELECT_INPUT = 0;
// Tell XBAR to dDMA on Rising
XBARA1_CTRL0 = XBARA_CTRL_STS0 | XBARA_CTRL_EDGE0(1) | XBARA_CTRL_DEN0;
// connect the IOMUX_XBAR_INOUT08 to DMA_CH_MUX_REQ30
xbar_connect(XBARA1_IN_IOMUX_XBAR_INOUT08, XBARA1_OUT_DMA_CH_MUX_REQ30);
// trigger our DMA channel at the request from XBAR
dmachannel.triggerAtHardwareEvent( DMAMUX_SOURCE_XBAR1_0 );
kickOffDMA();
}
void loop()
{
delay( 100 );
if ( dmaDone )
{
Serial.printf( "Counter %8d Buffer 0x%08X time %8u %s", counter, dmaBuffer[0], currTime - prevTime, error ? "ERROR" : "no error" );
if ( error )
{
Serial.printf( " [%d] 0x%08X 0x%08X", errorIndex, errA, errB );
}
Serial.printf( "\n");
dmaDone = false;
delay( 1000 );
Serial.printf( "Kicking off another \n" );
kickOffDMA();
}
else
{
Serial.printf( "Waiting...\n" );
}
++counter;
}
Once you feel confident with all this, reading any more complex setups will be much easier, as they generally build on these basics. For instance KurtE's camera code has two DMA setups, that alternate between one another, each writing to a different buffer. The DMA controller has a bunch of interesting functionality (like triggering interrupt on half-completion, so you can probably get a similar setup to KurtE's with just a single channel/buffer), it can be hooked up to different systems etc. And when you get some familiarity with these docs, it's actually not that hard to navigate them, even though they are over 3000 pages long.
And thanks to everyone around here, especially Paul and KurtE, all your work has been extremely helpful.