-
Notifications
You must be signed in to change notification settings - Fork 4
Option for statically allocated DMA buffer(s) for waveform generation in circular mode #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This is not true. The library allocates buffers from a special, contiguous DMA buffer pool, which can also be initialized statically. There's no constant allocation or de-allocation involved. When buffers get released, they return to the pool. And the library already uses double buffering modes in all drivers, and takes care of swapping buffers for the user code, cache maintenance etc... The user code only has to follow the examples and use the high-level API. |
Thank you for your quick response. I do not know the exact ins and outs of the implementation, I just cannot get my head around why one needs a pool of DMA buffers in the first place, and not just two. But it seems to be the case that one constantly has to poll for a new buffer from the queue to become available and to write the same data to it again and again. That seems to be quite inefficient, possibly wasting some processor power (The STM32H747 is quite fast, but still what is then the point of having a DMA in the first place?). At least that is the only method that is given in the examples from the high-level API documentation - see below. So all I am suggesting is that the user should be able to setup a single or two buffers, run the DAC and not having to check whether the DAC is available any longer. Wouldn't that be nice (from a user perspective)? If that is already possible then a hint on how to set this up would be highly appreciated. if (dac1.available()) {
// Get a free buffer for writing
SampleBuffer buf = dac1.dequeue();
// Write data to buffer (Even position: 0, Odd position: 0xFFF)
for (int i = 0; i < buf.size(); i++) {
buf.data()[i] = (i % 2 == 0) ? 0 : 0xFFF;
}
// Write the buffer to DAC
dac1.write(buf);
} |
If you can process the buffers fast enough then two buffers would work, but that's not always the case, especially if you do some processing in the sketch. The pool lets you create variable-sized FIFOs of DMA buffers so that the DMA never underruns/overruns. And more importantly, the pool takes care of the pitfalls and details that the user would have to implement otherwise. Such as aligning buffers to cache-lines, cache maintenance, etc.. Some of this may also need to be portable across different platforms (for example, the size of the cache line, if there's any cache). That all said, you could always allocate a small pool of just 2 or 3 buffers (the minimum number for output to start). The overhead is very small. Also with the latest update in core-API, the pool can be initialized from a static memory block. It will still do some dynamic allocation on start, for the buffers wrappers, but this lets you use a block of RAM for the pool's memory that might not be accessible by
Yes if the data doesn't change it has to be written over and over, but again that's just one use case. Normally when you use this library you have generate or consume data on the fly.
I didn't test this before, but you can write all the buffers in the pool initially in dac1.write(dac1.dequeue()); Note the data won't be changed, buffers don't get reset when they released they just return to the queue of free buffers. We could look into making this more standard, for example: fill buffers -> run forever. |
That sounds very interesting! I am not sure though whether the user can get a direct handle to all of these buffers, once created. Are these globablly defined? Or would it be the case of copying the buffer during a manual setup stage n (number of buffer) times, after which the user would simply call dac1.write(dac1.dequeue) as suggested? A function like fill buffers would be really fantastic! Would there be also a possibility to attach a callback function that could be called whenever the DAC would be ready to dequeue the next buffer? But I guess that I may know already the answer to that as the producer - consumer design pattern would probably not lend itself to such approach... |
The following should work. Note that you need exactly 3 buffers, the DAC starts after you write 3 buffers: // This example outputs an 8KHz square wave on A12/DAC0.
#include <Arduino_AdvancedAnalog.h>
AdvancedDAC dac1(A12);
void setup() {
Serial.begin(9600);
while (!Serial) {
}
if (!dac1.begin(AN_RESOLUTION_12, 16000, 32, 3)) {
Serial.println("Failed to start DAC1 !");
while (1);
}
for (int b=0; b<3; b++) {
// Get a free buffer for writing.
SampleBuffer buf = dac1.dequeue();
// Write data to buffers.
for (int i=0; i<buf.size(); i++) {
buf.data()[i] = (i % 2 == 0) ? 0: 0xfff;
}
dac1.write(buf);
}
}
void loop() {
if (dac1.available()) {
dac1.write(dac1.dequeue());
}
} |
Wow, that is really great! I have successfully tested your example on a Giga. This is a much better solution, and probaly would be ideal if the number of samples was large and if the dac.available() check could run in its own thread. Please feel free to close this issue as your directions were extremely helpful. But I would appreciate if a more automated feature (a loop parameter for auto-filling buffers, callbacks etc.) could be incorporated in the future. In the meantime I have posted your solution on the Arduino Forum where I had discussed this issue earlier. |
You can set any number of samples you want (within reason of course, the total pool size can't exceed the available memory).
I have a work in progress loop mode here #75 This allows you to have as many buffers as you want, you fill all buffers in #include <Arduino_AdvancedAnalog.h>
AdvancedDAC dac1(A12);
void setup() {
Serial.begin(9600);
while (!Serial) {
}
// Start DAC in loop mode.
if (!dac1.begin(AN_RESOLUTION_12, 16000, 32, 16, true)) {
Serial.println("Failed to start DAC1 !");
while (1);
}
// In loop mode, the DAC will start automatically after all
// buffers are filled, and continuously cycle through over all buffers.
uint16_t sample = 0;
while (dac1.available()) {
// Get a free buffer for writing.
SampleBuffer buf = dac1.dequeue();
// Write data to buffer.
for (int i=0; i<buf.size(); i++) {
buf.data()[i] = sample;
}
// Write the buffer to DAC.
dac1.write(buf);
sample += 256;
}
}
void loop() {
// In loop mode, no other DAC functions need to be called.
} |
I understand that the implementation will take some time, but I am not in a hurry as you have shown me a good workaraound. I am still assessing the Arduino as a possible platform for the STM32H747. I had very bad experiences prior due to unrecoverable MCUs (and the bootloader via VCP only seems to work on the ST Link v3, only on the internal module on the discovery board). But the Arduino Giga has a direct DFU connection, which is convenient and works well. I have earlier worked with TouchGFX, but there should be good lvgl tools around that could integrated nicely with the Arduino. I may contact those developers when building up a GUI. Anyhow, I really appreciate your help in this and that your decision to work on this new feature! |
Thanks, fantastic job and thank you for taking in my suggestion! |
You're welcome! |
Currently there are a larger number of buffers constantly allocated and de-allocated whenever the waveform is updated. This could be necessary for playing back a waveform, but this becomes very wasteful when dealing with simple periodic waveform generation (such as for a sine/cosine/triangle, etc.). For that only two fixed DMA buffers would suffice; one could switch the pointers between one or the other buffers for example when half of the buffer is reached, using an interrupt. The user does not even have to actively re-set the pointer in the interrupt service routine (not like for the Arduino Due)
I think that the key for making this work is enabling the circular mode (see below). This works well in the example code run on a STM32H747I-DISCO platform, but it would be nice if the Arduino_AdvancedAnalog API could be extended, allowing the user to start circular waveform generation. Once started, the MCU would not need to be engaged any longer. All the user would need to do is to declare and fill a buffer with a defined waveform and start a dedicated .begin method (or circular = true argument or similar).
I kindly ask you to consider this feature for implementation, or at least to start a discussion. I believe that such feature would be beneficial for many users.
The text was updated successfully, but these errors were encountered: