ESP32 xQueue, how to copy struct (memory) efficiently ?

Ok first some background info. I have a rather big sketch in which i store large portion of data onto an SD-card at times (recording a stream of data) Memory is a scarce resource up to some extend. (I have enough but i am trying not to waste any, more on that later) and since the SD-card writing is relatively slow and there are also other reception processes going on, i would like to use the 2nd Core of the ESP32 to do this, so i can minimize the impact of the SD-writing process.

With 2 cores not being able to access the same part of memory at the same time i decided that i should use a Queue, which seems the most appropriate way of transferring data between the main thread and a task running on the other (or even the same but that does not apply here) core.

Now i tend to get a little confused with structs pointers and arrays. the xQueue uses memcpy() internally i have been given to understand, so the actual data is being copied, not just the pointers to that, which is exactly what i want, but i don't want to first declare an 'extra' struct, copy into that and then put it in the Queue, that seems wasteful.

I mean, this compiles and should work

#define BLOCK_SIZE 512
#define SD_QUEUE_SIZE 16 // maybe only 8

typedef struct {
  uint16_t len;
  uint8_t block[BLOCK_SIZE];
} SdBlock;

  QueueHandle_t SdQ;
  QueueHandle_t SdFileName;
  QueueHandle_t SdError;

void setup() {
  Serial.begin(500000);
  SdQ = xQueueCreate(SD_QUEUE_SIZE, sizeof(SdBlock));
 
  if(SdQ == NULL){
    Serial.println("Error creating the queue");
  }
}

void loop() { 

}


void SendToTask(uint8_t* buf, uint16_t len) {
  SdBlock qBlock;
  memcpy (qBlock.block, buf, len);
  qBlock.len = len;
  xQueueSend(SdQ, &qBlock, portMAX_DELAY);
}

But the 'extra' qBlock declared is the part i think is wasteful.
Still, not all of the blocks in the Queue will be the same size, so i will need to include the amount of data as part of the block. If i don't need to declare that extra qBlock, i can add it to the Queue which will improve the buffer and therefore the flow of the main processing. So anybody have any idea on how to manage this, or do i just have to accept that i have to create and do an extra memcpy(), which also seems like a bit of a waste of (processing) time.

if the len varies, probably better to change block to a pointer..
the sender will need to malloc(len) and the receiver should free the pointer..

#define BLOCK_SIZE 512
#define SD_QUEUE_SIZE 16 // maybe only 8

typedef struct {
  uint16_t len;
  uint8_t* block;
} SdBlock;

  QueueHandle_t SdQ;
  QueueHandle_t SdFileName;
  QueueHandle_t SdError;

void setup() {
  Serial.begin(500000);
  SdQ = xQueueCreate(SD_QUEUE_SIZE, sizeof(SdBlock));
 
  if(SdQ == NULL){
    Serial.println("Error creating the queue");
  }
}

void loop() { 

}


void SendToTask(uint8_t* buf, uint16_t len) {
  SdBlock qBlock;
  qBlock.block = (uint8_t*)malloc(len);
  memcpy (qBlock.block, buf, len);
  qBlock.len = len;
  xQueueSend(SdQ, &qBlock, portMAX_DELAY);
}

good luck.. ~q

I am trying to prevent that, since that would mean the data will need to be retained at the 'sender' side of things and i have to keep an eye on that myself. Putting the data in the Queue, means that once it's in there i can do whatever i want with the source within the main thread.
It is a bit of a conundrum, i would want to copy directly to the Queue but both fields of the struct individually, and i just can't think of a way to do that.

The whole thing will be partly blocking in nature i guess.

Firstly i intend to add a 1 field Queue for the filename.
I send the Filename, and then send an empty string to the same Queue, which will hold the thread until at least the filename has been read, Then i send another empty string which holding the main thread again, giving a chance to the receiving task to send back any error codes it may have encountered. Only after these have been sent (if any) will the first empty string be received (and straight away the 2nd empty string) and will the main tread continue.
The file will be opened for WRITE, and anything in the Block Queue will be written to it until the Queue is empty and the file will be closed. As long as there is nothing in the Filename Queue, the moment that anything is found in the Block Queue will be appended to the file which name it has, if any errors are encountered they will be sent to the Error Queue which should periodically be read, although no infinite wait for available space in that is required i guess.

The extra block in the receiving task doesn't really matter all that much, that sort of is part of the buffering, and does add to the Queue, be it only locally.

The goal is to not hold the main thread with the SD writing process and let it continue with the data reception (UDP packets usually) or generation
If the receiving task finds that Queue available, it will read it,

The length will always vary or i will anyway have to maintain whatever leftover bytes there are in a variable, i guess there is no 2 ways about it really. Thank you for your help.

But are you doing that anyway? Do you need to access the data again after you want it written to the SD? In fact, you can read it again -- access to memory is serialized and therefore safe -- there just can't be multiple readers simultaneously (which of course means "at the same time" :slight_smile: -- verbatim, I just looked it up). How much slower is it if two tasks contend to read the same memory?

It's worth noting that many of the code examples, like for xQueueCreate, say

 // Create a queue capable of containing 10 pointers to AMessage structures.
 // These should be passed by pointer as they contain a lot of data.

So don't pass big structures. In the example, the (not-packed) struct is 24 bytes; that's "a lot". That means 512 for a sector (plus 2) is ginormous.

Having separate queues for the filename and file data seem problematic. The ideal case would have a single structure, for which you can then allocate some variable amount of space -- as long as you know how much in advance

struct Stuff {
  char filename[14];  // up to 13 chars, plus NUL
  uint16_t length;
  uint8_t data[0];
};
static_assert(offsetof(Stuff, data) == 16);  // 32-bit aligned, no space to pack

Stuff *allocStuff(uint16_t length) {
  auto ret = static_cast<Stuff*>(malloc(sizeof(Stuff) + length));
  if (ret) {
    ret->filename[0] = '\0';
    ret->length = length;
  }
  return ret;
}

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

void loop() {
  Stuff *x = allocStuff(30);
  Stuff *y = allocStuff(100);
  strcpy(x->filename, "ten");
  strcpy(reinterpret_cast<char*>(y->data), "eleven");  // hack to get something visible in there
  Serial.println(reinterpret_cast<uintptr_t>(x), HEX);
  Serial.println(reinterpret_cast<uintptr_t>(y), HEX);
  Serial.println(reinterpret_cast<uintptr_t>(y->data), HEX);
  Serial.println(x->filename);
  Serial.println(y->length);
  Serial.println(reinterpret_cast<char*>(y->data));
  free(x);
  free(y);
  delay(1500);
}

The filename can be empty for "same file"; maybe have a magic value for "done with the current file". You can then push those pointers onto the queue, and the next task can access the data. If the SD writer is the last thing, it can free the block. If it needs to stick around and needs to be managed by the main task (or another cleanup task), then maybe there is a bool or bit in the struct to indicate that the write is done.

The main setup+loop task runs on the "APP" core (core #1). The "PRO"-for-protocol core, core #0, handles the WiFi and Bluetooth (and more). Those high-priority tasks running on the "other" core are grabbing bytes out the air. If you're going to run the SD writer there as well, let us know how it works out if you run it at the the same high priority, where it will be time-sliced with the normal protocol stuff, to make those bytes available on the APP core; or at a lower priority.

toying around with this idea..

/*

https://forum.arduino.cc/t/esp32-xqueue-how-to-copy-struct-memory-efficiently/1393342

*/


#define BLOCK_SIZE 512
#define SD_QUEUE_SIZE 16 // maybe only 8

typedef struct {
  uint8_t  command;
  uint16_t len;
  uint8_t* block;
} SdBlock;

QueueHandle_t SdQ;

uint8_t SomeData[100];

void setup() {
  Serial.begin(115200);

  SdQ = xQueueCreate(SD_QUEUE_SIZE, sizeof(SdBlock));
  if ( SdQ != NULL )
  {
    xTaskCreate( vSdTask, "SD", 1000, NULL, 2, NULL );
  }
  else
  {
    /* The queue could not be created. */
    Serial.println("queue create failed..");
  }

  Serial.println("Ready..");

}

void loop() {


  if (Serial.available()) {
    switch (Serial.read()) {
      case '0': OpenFile("file.txt"); break;
      case '1': CloseFile(); break;
      case '2': WriteBlock((uint8_t*)&SomeData, sizeof(SomeData)); break;
    }
  }
  yield();
}



static void vSdTask( void *pvParameters )
{
  char CurrFileName[14];
  SdBlock RecBlock;
  BaseType_t xStatus;
  const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );

  for ( ;; )
  {
    xStatus = xQueueReceive( SdQ, &RecBlock, xTicksToWait );
    if ( xStatus == pdPASS )
    {
      switch (RecBlock.command) {
        case 0: //open file
          if (RecBlock.len < 14) {
            strcpy(CurrFileName, (char*)RecBlock.block);
            Serial.print("Opening File:"); Serial.println(CurrFileName);
          }
          break;
        case 1: //close file
          Serial.println("Closing File"); Serial.println(CurrFileName);
          break;
        case 2: //write block
          Serial.print("Writing block size:");
          Serial.println(RecBlock.len);
          break;

      }

      if (RecBlock.len > 0) {
        free(RecBlock.block);
        RecBlock.len = 0;
      }
    }
    else
    {
      vTaskDelay(1);
    }
    yield();
  }
}




bool OpenFile(char* filename) {
  BaseType_t qStatus;
  const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
  SdBlock qBlock;
  bool ret = true;
  if (strlen(filename) > 0) {
    qBlock.block = (uint8_t*)malloc(strlen(filename) + 1);
    strcpy((char*)qBlock.block, filename);
    qBlock.command = 0;
    qBlock.len = strlen(filename);
    qStatus = xQueueSend(SdQ, &qBlock, xTicksToWait);
    if (qStatus != pdPASS) {
      Serial.println("OF:Send to Q failed..");
      free(qBlock.block);
      ret = false;
    }
  } else ret = false;
  return ret;
}

bool CloseFile() {
  BaseType_t qStatus;
  const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
  SdBlock qBlock;
  bool ret = true;
  qBlock.command = 1;
  qBlock.len = 0;
  qStatus = xQueueSend(SdQ, &qBlock, xTicksToWait);
  if (qStatus != pdPASS) {
    Serial.println("CF:Send to Q failed..");
    ret = false;
  }
  return ret;
}


bool WriteBlock(uint8_t* buf, uint16_t len) {
  BaseType_t qStatus;
  const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
  SdBlock qBlock;
  bool ret = true;
  if (len > 0) {
    qBlock.block = (uint8_t*)malloc(len);
    memcpy (qBlock.block, buf, len);
  }
  qBlock.len = len;
  qBlock.command = 2;
  qStatus = xQueueSend(SdQ, &qBlock, xTicksToWait);
  if (qStatus != pdPASS) {
    Serial.println("WB:Send to Q failed..");
    if (len > 0) free(qBlock.block);
    ret = false;
  }
  return ret;
}

fun stuff.. ~q

1 Like

The problem is that i want to copy (LED) pixeldata to the SD. This pixeldata is available the moment that i start processing it for transmitting it to the addressable LEDs. The transmission itself happens using I2S and so the main pixelbuffer would be available straightaway to start receiving new data into, the moment the actual transmission starts. If i am to write this main pixelbuffer also onto an SD-card, i can not start writing to that buffer until the whole buffer has been written. The SD-card is significantly slower than doing memcpy(). The whole pixelbuffer may be as big as 2720 pixel = 8160 bytes. I could just copy the whole thing + the timecode which also needs to be included, but with the double buffer at the receiving end (and at the transmitting end due to the timecode) the waste of memory appears to be just a bit to much for the program i have at the moment. Memory is a fairly scarce resourse, partly due to the use of I2S for the LED signal. Hence the copying of 512 byte blocks.
I have been given to understand that the writing to the SD card happens also in that block size. (more or less)
So anyway the data needs to be copied so the buffer can be written to again (which is the first process that happens after it has been processed for transmission) The heap is already being used dynamically by the webserver and some other small things and is managed manually, so i don't really want to start declaring blocks of data onto it and freeing them up again. the xQueue will declare it's own dedicated area. I am hoping that i will have enough memory left over to declare a 16-block Queue, which would be a complete frame, but even if i end up having only half of that, it still means that after half of the writing to the SD-card is complete, the main processor is free to start receiving new data.

Yes it does, but having a 30 byte section of a structure in a 16 block Queue ends up being 480 bytes.

Having just the actual data and it's size is intended for efficiency reasons at the receiving end. The file is created, it has a small header written to it, this is all not very time sensitive. Then the blocks start arriving and they should just be written as is. Once the 'recording' is complete, timing is no longer sensitive. No more block will be added to the Queue, the process of receiving from the Queue doesn't need to happen, and the filename can be cleared. Any blocks that get written to the Queue by mistake will be written and discarded (this really should not be possible, but i may as well have that part of the process still running, it doesn't take much)

For a more universal solution i would go that way, since that would allow for writing to multiple files as well and actually would be a really nice library to have available on an ESP32. It could probably be even more efficient if it would be a part of the SD library.

Since i started out on the main project on a devBoard and had that selected as my board, i had actually missed the setting, and both the APP as the Events were running on core 1 until i changed that. It didn't actually cause to much issue until i started playback. Reading from the SD-card is also not that fast. That said, BT is not used at all, and WiFi is only used for the webserver and the main thing interfering is actually WiFi maintaing it's connections to devices, the UDP packages are coming in over an Ethernet connection through an SPI bus and are happening on the main core. It is just to much data for WiFi to do this reliably. I am intending to fiddle with the priority a bit, i guess it doesn't need to be all that high, it is an unknown as yet.

Yeah the thoughts about it also tickle me. It would be nice to create a universal solution, though i would avoid declaring on the heap to avoid memory fragmentation.
If one is to use the heap to transfer the date, then it should be done with a fixed size block at compile time, and never really be freed again. Otherwise end users may not safely use the heap anymore at all.

Yes it is, but it anyway needs to be buffered, why not let this API do that ?

I was actually planning to just close the file if the Queue is empty and open it again. Apparently there is time to do so if the Queue is empty.
Leaving the file open is faster in theory, but in practice with these huge amounts of data, the differences are marginal, and anyway if there is time, it is a good idea to consolidate the file.

Thinking about that i think i can even use the same Queue to read and buffer. When reading happens there is less processing happening overall, there is no reception processing, but i actually suspect that reading is even slower than writing to an SD. Another experiment to think about.

It's already really close to having the complete solution for what i am trying to do. There is still a memory saving that i think i can do in the outgoing I2S stream (3-step instead of 4-step) but the processing for it is to slow as yet and will need to be re-written optimized for speed (3-step should be nearly as fast as 4-step but as it is now it is about 4x slower)

Ok so i finished a testing sketch which more or less implements how i thought it up initially.
Instead of blocking the main thread using the Filename Queue i decided it was easier to let the Error Queue respond. It isn't the prettiest yet, and some things still need to be modified.

#include "FS.h"
#include "SD.h"

#define HSPI_SCK 17
#define HSPI_MISO 16
#define HSPI_MOSI 4
#define SD_CS 15  // Added 10K pullup for v2 (strapping pin must be HIGH)

#define LED_WHITE 14

#define SPI_FRQ 32000000

#define SERIAL_OUTPUT_BAUD 500000
#define PIXEL_BYTES (680 * 3)
#define PORTS 4
#define FRAMES (40 * 60 * 2) // 40 Hz, 60 Seconds, 2 Minutes

#define FILENAME_LENGTH 30
#define BLOCK_SIZE 512
#define SD_QUEUE_SIZE 32 // maybe only 8
#define SD_ERR_QUEUE_SIZE 16

#define NO_ERROR 0
#define UNEXP_SDQ 1
#define UNEXP_SDQ_EMPTY 2
#define UNEXP_FILENAME_EMPTY 3
#define UNEXP_FILENAME_NULL 4
#define FILE_NOT_CREATED 5
#define NAME_RECEIVED 6
#define EMPTY_QUEUE 7
#define WRITE_ERROR 8
#define HEADER_WRITTEN 9
#define WRITE_STOP 10
#define NAME_NOT_EXP 11
#define Q_NOT_EMPTY 12
#define STACK_SIZE 13

#define WAIT_FOR_NAME 0
#define WAIT_FOR_DATA 1

#define STACK_CHECK //{uint32_t stack = uxTaskGetStackHighWaterMark(NULL); if (stack < maxStack) maxStack = stack;}


typedef struct {
  uint16_t len;
  uint8_t block[BLOCK_SIZE];
} SdBlock;

QueueHandle_t SdQ;
QueueHandle_t SdFileName;
QueueHandle_t SdError;

SPIClass * hspi = NULL;

uint8_t stripixels[PORTS][PIXEL_BYTES];
uint16_t lengths[PORTS] = {PIXEL_BYTES, PIXEL_BYTES, PIXEL_BYTES, PIXEL_BYTES};
uint16_t totalLengths = 0;

void setup() {
  Serial.begin(500000);
  delay(1000);
  Serial.println();
  Serial.println();
  Serial.println("SD Task on core 0 test");
  pinMode(LED_WHITE, OUTPUT);
  SdQ = xQueueCreate(SD_QUEUE_SIZE, sizeof(SdBlock));
  SdFileName = xQueueCreate(1, FILENAME_LENGTH + 1);  // filename Queue should be only 1 so the main thread can be blocked
  uint8_t err = 0;
  SdError = xQueueCreate(SD_ERR_QUEUE_SIZE, sizeof(err)); // error codes are only 1 byte

  if (SdQ == NULL) {
    Serial.println(F("Error creating the SD queue"));
  }
  if (SdFileName == NULL) {
    Serial.println(F("Error creating the finename queue"));
  }
  if (SdError == NULL) {
    Serial.println(F("Error creating the SD Error queue"));
  }

  if (InitSdCard()) {
    Serial.println(F("SD card initialized"));
  }

  xTaskCreatePinnedToCore(
    HandleQueueToSD,
    "Task Write To SD",  // A name just for humans
    3000,  // The stack size can be checked by calling `uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);`
    NULL,  // No parameter is used
    2,  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    NULL,  // Task handle is not used here
    0  // pinned to core
  );

  uint8_t b = 0;
  for (uint8_t i = 0; i < PORTS; i++) {
    for (uint16_t j = 0; j < PIXEL_BYTES; j++) {
      stripixels[i][j] = b;
      b++;
    }
    totalLengths += lengths[i];
  }

  Serial.println(F("Arrays Filled."));
  Serial.print(F("Total Lengths : "));
  Serial.println(totalLengths, DEC);
  delay(1000);

  if (CreateSdFileToTask("TestxTaskSd_2")) {
    Serial.println(F("Created SD Ok."));
    uint32_t frames = FRAMES;
    uint32_t lastFrame = millis();
    int pinstate = HIGH;
    while (frames) {
      digitalWrite(LED_WHITE, pinstate);
      pinstate = HIGH - pinstate;
      frames--;
      while (millis() - lastFrame < 25) {  // blocking time limiter
        delayMicroseconds(10); // stability
      }
      lastFrame = millis();
      for (uint8_t i = 0; i < PORTS; i++) {
        if (!SendDataBlockToSdQueue(stripixels[i], lengths[i])) {
          frames = 0;
        }
      }
    } // frames
    if (StopSdFileToTask()) {
      Serial.println(F("Filename Cleared, Writing Stopped"));
      /*uint8_t err = 0;
      int ret = xQueueReceive(SdError, &err, portMAX_DELAY);
      if (err == STACK_SIZE) {
        uint32_t maxStack = 0;
        for (uint8_t i = 0; i < 4; i++) {
          ret = xQueueReceive(SdError, &err, portMAX_DELAY);
          maxStack |= ((uint32_t) err << (8 * i));
        }
        Serial.print(F("Minimum Free Stack Size : "));
        Serial.println(maxStack, DEC);
      }*/
    }
  }// file Created
}

void loop() {

}

void SendBlockToTask(uint8_t* buf, uint16_t len) {
  SdBlock qBlock;
  memcpy (qBlock.block, buf, len);
  qBlock.len = len;
  xQueueSend(SdQ, &qBlock, portMAX_DELAY);
}

void PrintAllErrorQueue() {
  int ret = pdPASS;
  uint8_t err = NO_ERROR;
  while (uxQueueMessagesWaiting(SdError)) {
    ret = xQueueReceive(SdError, &err, portMAX_DELAY);
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
  }
}

bool StopSdFileToTask() {
  PrintAllErrorQueue(); // flush the error Queue
  char nameString[FILENAME_LENGTH + 1] = {'\0'};
  xQueueSend(SdFileName, &nameString, portMAX_DELAY);  // send the noname
  while (!uxQueueMessagesWaiting(SdError)) {  // wait for response
    delayMicroseconds(10); // stability
  }
  uint8_t err;
  int ret = xQueueReceive(SdError, &err, portMAX_DELAY);
  if (err != WRITE_STOP) {
    Serial.println(F("'Write Stop' not received"));
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
    PrintAllErrorQueue();
    return false;
  }
  return true;
}

bool CreateSdFileToTask(String filename) {
  PrintAllErrorQueue(); // flush the error Queue
  char nameString[FILENAME_LENGTH + 1] = {'\0'};
  filename = "/" + filename;
  filename += ".pst";  // adding the extension here is better
  uint8_t len = filename.length();
  if (len > FILENAME_LENGTH) len = FILENAME_LENGTH;
  filename.toCharArray(nameString, len + 1);
  xQueueSend(SdFileName, &nameString, portMAX_DELAY);  // send the name

  while (!uxQueueMessagesWaiting(SdError)) {  // wait for response
    delay(10); // stability
  }
  uint8_t err;
  int ret = xQueueReceive(SdError, &err, portMAX_DELAY);
  if (err != NAME_RECEIVED) {
    Serial.println(F("Name not received"));
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
    PrintAllErrorQueue();
    return false;
  }

  char header[] = "DmagiX PixelStream vx.xx\rColorSequence=X\rFrameSize=0000\rDmxSize=000\rID=000000#";
  SdBlock qBlock;
  memcpy (qBlock.block, header, sizeof(header) - 1);
  qBlock.len = sizeof(header) - 1;
  xQueueSend(SdQ, &qBlock, portMAX_DELAY); // send the header to the SD Queue

  ret = xQueueReceive(SdError, &err, portMAX_DELAY);
  if (err != HEADER_WRITTEN) {
    Serial.println(F("Header not written"));
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
    PrintAllErrorQueue();
    return false;
  }

  ret = pdPASS;
  err = NO_ERROR;
  while (uxQueueMessagesWaiting(SdError) && (ret == pdPASS)) {
    ret = xQueueReceive(SdError, &err, portMAX_DELAY);
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
  }
  if (err != NO_ERROR) return false;
  delay(10);
  return true; // success
}

bool SendDataBlockToSdQueue(uint8_t * block, uint16_t len) {
  uint16_t ndx = 0;
  while ((len) && (!uxQueueMessagesWaiting(SdError))) {  // break also on error message waiting
    uint16_t blockSize = 512;
    if (blockSize > len) blockSize = len;
    SdBlock qBlock;
    memcpy (qBlock.block, block + ndx, blockSize);
    qBlock.len = blockSize;
    xQueueSend(SdQ, &qBlock, portMAX_DELAY); // send the block to the SD Queue
    len -= blockSize;
    ndx += blockSize;
  }
  int ret = pdPASS;
  uint8_t err = NO_ERROR;
  while (uxQueueMessagesWaiting(SdError) && (ret == pdPASS)) {
    ret = xQueueReceive(SdError, &err, portMAX_DELAY);
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
  }
  if (err != NO_ERROR) return false;
  return true; // success
}

bool InitSdCard() {
  if (hspi == NULL) {
    hspi = new SPIClass(HSPI);
  }
  hspi->begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, SD_CS);
  hspi->setFrequency(SPI_FRQ);

  if (!SD.begin(SD_CS, *hspi, SPI_FRQ)) {
    Serial.println(F("No SD card found"));
    return false;
  }
  return true;
}

//-----------------------------------------------------------------------------------

void HandleQueueToSD(void *pvParameters) {
  SdBlock taskBlock;
  char nameString[FILENAME_LENGTH + 1] = {'\0'};
  uint8_t taskState = WAIT_FOR_NAME;
  File sFile;
  //uint32_t maxStack = uxTaskGetStackHighWaterMark(NULL);

  for (;;) {
    
    vTaskDelay(1 / portTICK_PERIOD_MS);  // when running a higher priority
    
    if ((SdQ != NULL) && (SdFileName != NULL) && (SdError != NULL))  {  // Sanity check
      
      //STACK_CHECK
      
      switch (taskState) {
        //STACK_CHECK
        case WAIT_FOR_NAME: {
            if (nameString[0] == '\0') {  // only if we have no name

              if (uxQueueMessagesWaiting(SdQ)) {  // shouldn't happen, but we need to read them regardless
                int ret = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0);  // it should be there, if it isn't we can continue
                if (ret == pdPASS) {  // and send error messages back
                  uint8_t err = UNEXP_SDQ;
                  xQueueSend(SdError, &err, (TickType_t) 0); // don't wait
                  //STACK_CHECK
                }
                else if (ret == pdFALSE) {
                  uint8_t err = UNEXP_SDQ_EMPTY;
                  xQueueSend(SdError, &err, (TickType_t) 0); // don't wait
                  //STACK_CHECK
                }
              }

              if (uxQueueMessagesWaiting(SdFileName)) {
                int ret = xQueueReceive(SdFileName, &nameString, portMAX_DELAY);  // read the filename
                if (ret == pdPASS) {
                  //STACK_CHECK
                  if (nameString[0] == '\0') {
                    uint8_t err = UNEXP_FILENAME_NULL;
                    xQueueSend(SdError, &err, (TickType_t) 0);  // should not be empty
                  }
                  else {
                    //STACK_CHECK
                    sFile = SD.open(nameString, FILE_WRITE);  // open the file
                    if (!sFile) {
                      uint8_t err = FILE_NOT_CREATED;
                      xQueueSend(SdError, &err, (TickType_t) 0);  // error
                    }
                    else {
                      //STACK_CHECK
                      uint8_t err = NAME_RECEIVED;
                      xQueueSend(SdError, &err, (TickType_t) 0);  // No error !!
                      while (!uxQueueMessagesWaiting(SdQ)) ;
                      //STACK_CHECK
                      int ret = xQueueReceive(SdQ, &taskBlock, portMAX_DELAY);
                      if (ret == pdFALSE) {
                        uint8_t err = EMPTY_QUEUE;
                        xQueueSend(SdError, &err, (TickType_t) 0);
                        //STACK_CHECK
                        sFile.close();
                        nameString[0] = '\0';
                      }
                      else {
                        //STACK_CHECK
                        uint16_t bytesWritten = sFile.write(taskBlock.block, taskBlock.len);
                        if (bytesWritten != taskBlock.len) { // error
                          uint8_t err = WRITE_ERROR;
                          //STACK_CHECK
                          xQueueSend(SdError, &err, (TickType_t) 0);
                          nameString[0] = '\0';
                        }
                        else {
                          uint8_t err = HEADER_WRITTEN;  // no error
                          //STACK_CHECK
                          xQueueSend(SdError, &err, (TickType_t) 0);
                          taskState = WAIT_FOR_DATA;
                        }
                        //STACK_CHECK
                        sFile.close();  // close file anyway
                      }
                    }
                  }
                }
                else if (ret == pdFALSE) {
                  //STACK_CHECK
                  uint8_t err = UNEXP_FILENAME_EMPTY;
                  xQueueSend(SdError, &err, (TickType_t) 0); // don't wait
                }
              }
            }
          }

        case WAIT_FOR_DATA: {
            //STACK_CHECK
            if (uxQueueMessagesWaiting(SdQ)) {
              sFile = SD.open(nameString, FILE_APPEND);  // open the file
              int ret = pdPASS;

              while (ret == pdPASS) {
                //STACK_CHECK
                ret = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0); //portMAX_DELAY);
                if (ret == pdPASS) {
                  uint16_t bytesWritten = sFile.write(taskBlock.block, taskBlock.len);
                  if (bytesWritten != taskBlock.len) { // error
                    uint8_t err = WRITE_ERROR;
                    //STACK_CHECK
                    xQueueSend(SdError, &err, (TickType_t) 0); // send the error

                    // send specifics
                    err = bytesWritten >> 8;
                    xQueueSend(SdError, &err, (TickType_t) 0); 
                    err = bytesWritten & 0xFF;
                    xQueueSend(SdError, &err, (TickType_t) 0); 
                    err = taskBlock.len >> 8;
                    xQueueSend(SdError, &err, (TickType_t) 0); 
                    err = taskBlock.len & 0xFF;
                    xQueueSend(SdError, &err, (TickType_t) 0);

                    
                    nameString[0] = '\0';  // clear the name
                    taskState = WAIT_FOR_NAME;  // back to the name task
                    ret = pdFALSE; // Stop write loop
                    int fl = pdPASS;
                    while (fl == pdPASS) {  // flush the Queue
                      //STACK_CHECK
                      fl = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0);
                    }
                  }
                  STACK_CHECK
                }
              } // read Queue loop
              //STACK_CHECK
              sFile.close();
              //STACK_CHECK
            }  // if SdQ available

            if (uxQueueMessagesWaiting(SdFileName)) {  // check if there is a name available
              int ret = xQueueReceive(SdFileName, &nameString, portMAX_DELAY);  // read the filename
              if (ret == pdPASS) {
                //STACK_CHECK
                if (nameString[0] == '\0') {
                  uint8_t err = WRITE_STOP;
                  xQueueSend(SdError, &err, (TickType_t) 0);  // should be empty
                  //STACK_CHECK
                }
                else {   // we were in a stream, so the name shoyuld have been empty
                  uint8_t err = NAME_NOT_EXP;
                  xQueueSend(SdError, &err, (TickType_t) 0);  // not empty
                  nameString[0] = '\0';
                  //STACK_CHECK
                }
                taskState = WAIT_FOR_NAME; // task ends either way
                int q = pdPASS;
                bool notEmpty = false;
                //STACK_CHECK
                while (q == pdPASS) {  // flush the Queue just in case (should be empty)
                  q = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0);
                  if (q == pdPASS) {
                    notEmpty = true;
                    //STACK_CHECK
                  }
                }
                if (notEmpty) { // the queue really should be empty
                  uint8_t err = Q_NOT_EMPTY;
                  //STACK_CHECK
                  xQueueSend(SdError, &err, (TickType_t) 0);  // should be empty
                }
                
                // send the maxStack

                /*uint8_t err = STACK_SIZE;
                xQueueSend(SdError, &err, (TickType_t) 0);
                err = maxStack & 0xFF;
                maxStack = maxStack >> 8;
                xQueueSend(SdError, &err, (TickType_t) 0);
                err = maxStack & 0xFF;
                maxStack = maxStack >> 8;
                xQueueSend(SdError, &err, (TickType_t) 0);
                err = maxStack & 0xFF;
                maxStack = maxStack >> 8;
                xQueueSend(SdError, &err, (TickType_t) 0);
                err = maxStack & 0xFF;
                maxStack = maxStack >> 8;
                xQueueSend(SdError, &err, (TickType_t) 0);
                maxStack = uxTaskGetStackHighWaterMark(NULL);
                */
              }
            }
          }  // wait for data
      }
    }  // Sanity check
  }  // Infinite loop
}

Now when i was trying to work out how much Stack Size to declare, the indication was that about 2800 bytes were required, but when i set it to that, it started crashing until i commented out the measuring of the stack size !?
I don't see how that could require an extra 3000 bytes but that much more was what was required for a stable run.

The idea was anyway to create a buffer which could be flexible.

Other option could be to allocate a buffer for all the pixels on the heap. That way i could do all of the processing for what to store on the SD card on core 0, but that would mean that the buffer isn't flexible anymore. As it is, the buffer is partly available for writing when blocks have already been written to the SD card, which should make the less of an impact on the overall performance.

Anyway. This is the first testing of it, now i guess it's time to implement this is a bigger sketch and see how it works for me. The bigger the SD Queue the smoother i suspect.

Oh yeah something else that was rather odd showed up.

uint16_t bytesWritten = sFile.write(taskBlock.block, taskBlock.len);
if (bytesWritten != taskBlock.len) { // error
  uint8_t err = WRITE_ERROR;

This error condition got triggered repeatedly, (not always but every 2nd or 3rd time i ran the sketch.)
I usually just test if it isn't '0'

Shouldn't be possible of course, but when i started to investigate

err = bytesWritten >> 8;
xQueueSend(SdError, &err, (TickType_t) 0); 
err = bytesWritten & 0xFF;
xQueueSend(SdError, &err, (TickType_t) 0); 
err = taskBlock.len >> 8;
xQueueSend(SdError, &err, (TickType_t) 0); 
err = taskBlock.len & 0xFF;
xQueueSend(SdError, &err, (TickType_t) 0);

It disappeared and never came back again, must be something that the optimizer just didn't do quite right, another mystery :slight_smile:

Did some small modification and a bit of clean up.

Since i am also including a 32 bit timecode as part of the frame, it seemed wasteful to send that as a separate part of the Queue, so instead i made the SdBlock static to the data transmission function.
And it won't be sent until it's full. I send the remaining when the function is called with a len == 0, Other solutions for that exist.

In effect this just becomes an extra part of the Queue, which is somehow what i originally was looking for. So i'm happy.

With the Queue at 32 blocks, there seems to be no moment when the writing slows down the processor due to the actual writing to the SD. Basically it should now be as fast as memcpy-twice (which is for sure quicker than the SD-card) so it should free up quite a bit of processing power.

Don't know what it means for the WiFi if it is also sharing that core, but testing may show up something or not.

The advantage of passing the name once and not with every block is in some way space.
This is tailored for a specific program, a slightly different method sending a name and Append or write
might be more appropriate.
Not closing the file while there is blocks in the buffer is an efficiency thing, and closing it when there is time, also makes sense.

Priorities might be something to fiddle with a bit as well.

I am considering to do the same thing for reading streams as well. Reading from the SD is also seems to be a bit of a bottleneck, though less than writing since no extra processing is required (no data creation or reception). The Queue can be used in either direction.

The error Queue is a must have. You need something to communicate between the cores. The main thread is in charge of what that other thread is supposed to e doing, and status reports are required or all hell might break lose.

#include "FS.h"
#include "SD.h"

#define HSPI_SCK 17
#define HSPI_MISO 16
#define HSPI_MOSI 4
#define SD_CS 15  // Added 10K pullup for v2 (strapping pin must be HIGH)

#define LED_WHITE 14

#define SPI_FRQ 80000000

#define SERIAL_OUTPUT_BAUD 500000
#define PIXEL_BYTES (680 * 3)
#define PORTS 4
#define FRAMES (40 * 60 * 1) // 40 Hz, 60 Seconds, 1 Minute

#define SD_TASK_PRIORITY 2  //
#define FILE_NAME "TestxTaskSd_7"

#define FILENAME_LENGTH 30
#define BLOCK_SIZE 512
#define SD_QUEUE_SIZE 32 // maybe only 8
#define SD_ERR_QUEUE_SIZE 16

#define NO_ERROR 0
#define UNEXP_SDQ 1
#define UNEXP_SDQ_EMPTY 2
#define UNEXP_FILENAME_EMPTY 3
#define UNEXP_FILENAME_NULL 4
#define FILE_NOT_CREATED 5
#define NAME_RECEIVED 6
#define EMPTY_QUEUE 7
#define WRITE_ERROR 8
#define HEADER_WRITTEN 9
#define WRITE_STOP 10
#define NAME_NOT_EXP 11
#define Q_NOT_EMPTY 12
#define STACK_SIZE 13

#define WAIT_FOR_NAME 0
#define WAIT_FOR_DATA 1


typedef struct {
  uint16_t len = 0;
  uint8_t block[BLOCK_SIZE];
} SdBlock;

QueueHandle_t SdQ;
QueueHandle_t SdFileName;
QueueHandle_t SdError;

SPIClass * hspi = NULL;

uint8_t stripixels[PORTS][PIXEL_BYTES];
uint16_t lengths[PORTS] = {PIXEL_BYTES, PIXEL_BYTES, PIXEL_BYTES, PIXEL_BYTES};
uint16_t totalLengths = 0;

uint32_t expSize;

void setup() {
  Serial.begin(500000);
  delay(1000);
  Serial.println();
  Serial.println();
  Serial.println("SD Task on core 0 test");
  pinMode(LED_WHITE, OUTPUT);
  QueueCreation();

  if (InitSdCard()) {
    Serial.println(F("SD card initialized"));
  }

  TaskCreation();
  uint8_t b = 0;
  for (uint8_t i = 0; i < PORTS; i++) {
    for (uint16_t j = 0; j < PIXEL_BYTES; j++) {
      stripixels[i][j] = b;
      b++;
    }
    totalLengths += lengths[i];
  }

  Serial.println(F("Arrays Filled."));
  Serial.print(F("Total Lengths : "));
  Serial.println(totalLengths, DEC);
  expSize = FRAMES * (totalLengths + 4);
  delay(1000);

  if (CreateSdFileToTask(FILE_NAME)) {
    Serial.println(F("Created SD Ok."));
    uint32_t frames = FRAMES;
    uint32_t lastFrame = millis();
    uint32_t firstFrame = lastFrame;
    int pinstate = HIGH;
    while (frames) {
      digitalWrite(LED_WHITE, pinstate);
      pinstate = HIGH - pinstate;
      frames--;
      while (millis() - lastFrame < 25);   // blocking time limiter

      //lastFrame += 25;  // i use these to see if a frame might be delayed
      lastFrame = millis(); 

      uint32_t moment = lastFrame - firstFrame;  // calculate the elapsed time from 1st frame
      uint8_t tc[4];        // timecode format i have been using, Big-endian, regardless of what
      uint8_t nr_bytes = 4;   // the processor uses.
      while (nr_bytes) {
        nr_bytes--;
        tc[3 - nr_bytes] = (moment >> (8 * nr_bytes)) & 0xFF;  //edit not 4 - nr_bytes, but 3 
      }
      if (!SendDataBlockToSdQueue(tc, 4)) {
        frames = 0;
      }

      for (uint8_t i = 0; i < PORTS; i++) {
        if (!SendDataBlockToSdQueue(stripixels[i], lengths[i])) {
          frames = 0;
        }
      }
    } // frames
    if (StopSdFileToTask()) {
      uint32_t totalTime = millis() - firstFrame;
      Serial.println(F("Filename Cleared, Writing Stopped"));
      Serial.print(F("Total elapsed time = "));
      Serial.println(totalTime, DEC);
    }
  }// file Created

  PrintFileSize(FILE_NAME);
}

void TaskCreation() {
  xTaskCreatePinnedToCore(
    HandleQueueToSD,
    "Task Write To SD",  // A name just for humans
    3000,  // The stack size can be checked by calling `uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);`
    NULL,  // No parameter is used
    SD_TASK_PRIORITY,  // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
    NULL,  // Task handle is not used here
    0  // pinned to core
  );
}

void QueueCreation() {
  SdQ = xQueueCreate(SD_QUEUE_SIZE, sizeof(SdBlock));
  SdFileName = xQueueCreate(1, FILENAME_LENGTH + 1);  // filename Queue should be only 1 so the main thread can be blocked
  uint8_t err = 0;
  SdError = xQueueCreate(SD_ERR_QUEUE_SIZE, sizeof(err)); // error codes are only 1 byte

  if (SdQ == NULL) {
    Serial.println(F("Error creating the SD queue"));
  }
  if (SdFileName == NULL) {
    Serial.println(F("Error creating the finename queue"));
  }
  if (SdError == NULL) {
    Serial.println(F("Error creating the SD Error queue"));
  }
}

void loop() {}

//----------------------------------------------------------------------------------

void PrintFileSize(String filename) {
  filename = "/" + filename;
  filename += ".pst";
  File sFile = SD.open(filename, FILE_READ);
  uint32_t filesize = sFile.size();
  sFile.close();
  Serial.print(F("File Size = "));
  Serial.println(filesize, DEC);
  Serial.print(F("Expected Size = "));
  Serial.println(expSize, DEC);
}


void PrintAllErrorQueue() {
  int ret = pdPASS;
  uint8_t err = NO_ERROR;
  while (uxQueueMessagesWaiting(SdError)) {
    ret = xQueueReceive(SdError, &err, portMAX_DELAY);
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
  }
}

bool StopSdFileToTask() {
  if (SendDataBlockToSdQueue(stripixels[0], 0)) {
    delay(1); // send the final block and wait a ms.
    Serial.println(F("Last Block sent !"));
  }
  else {
    Serial.println(F("Last Block not sent !"));
  }
  PrintAllErrorQueue(); // flush the error Queue
  char nameString[FILENAME_LENGTH + 1] = {'\0'};
  xQueueSend(SdFileName, &nameString, portMAX_DELAY);  // send the noname
  while (!uxQueueMessagesWaiting(SdError)) {  // wait for response
    delayMicroseconds(10); // stability
  }
  uint8_t err;
  int ret = xQueueReceive(SdError, &err, portMAX_DELAY);
  if (err != WRITE_STOP) {
    Serial.println(F("'Write Stop' not received"));
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
    PrintAllErrorQueue();
    return false;
  }
  return true;
}

bool CreateSdFileToTask(String filename) {
  PrintAllErrorQueue(); // flush the error Queue
  char nameString[FILENAME_LENGTH + 1] = {'\0'};
  filename = "/" + filename;
  filename += ".pst";  // adding the extension here is better
  uint8_t len = filename.length();
  if (len > FILENAME_LENGTH) len = FILENAME_LENGTH;
  filename.toCharArray(nameString, len + 1);
  xQueueSend(SdFileName, &nameString, portMAX_DELAY);  // send the name

  while (!uxQueueMessagesWaiting(SdError)) {  // wait for response
    delay(10); // stability
  }
  uint8_t err;
  int ret = xQueueReceive(SdError, &err, portMAX_DELAY);
  if (err != NAME_RECEIVED) {
    Serial.println(F("Name not received"));
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
    PrintAllErrorQueue();
    return false;
  }

  char header[] = "DmagiX PixelStream vx.xx\rColorSequence=X\rFrameSize=0000\rDmxSize=000\rID=000000#";
  SdBlock qBlock;
  memcpy (qBlock.block, header, sizeof(header) - 1);
  qBlock.len = sizeof(header) - 1;
  expSize += sizeof(header) - 1;
  xQueueSend(SdQ, &qBlock, portMAX_DELAY); // send the header to the SD Queue

  ret = xQueueReceive(SdError, &err, portMAX_DELAY);
  if (err != HEADER_WRITTEN) {
    Serial.println(F("Header not written"));
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
    PrintAllErrorQueue();
    return false;
  }

  ret = pdPASS;
  err = NO_ERROR;
  while (uxQueueMessagesWaiting(SdError) && (ret == pdPASS)) {
    ret = xQueueReceive(SdError, &err, portMAX_DELAY);
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
  }
  if (err != NO_ERROR) return false;
  delay(10);
  return true; // success
}

bool SendDataBlockToSdQueue(uint8_t * block, uint16_t len) {
  const uint16_t blockMax = 512;
  static SdBlock qBlock;
  uint16_t ndx = 0;

  if (len) {
    while ((len) && (!uxQueueMessagesWaiting(SdError))) {  // break also on error message waiting
      uint16_t blockSize = blockMax - qBlock.len;  // space available in the block
      if (blockSize > len) {  // if we have more available in the qBlock than what we have to copy.
        blockSize = len; // don't copy what we don't have
      }
      memcpy (qBlock.block + qBlock.len, block + ndx, blockSize);  // copy what needs to be copied
      qBlock.len += blockSize;
      len -= blockSize;
      ndx += blockSize;
      if (qBlock.len >= blockMax) {  // whole block
        xQueueSend(SdQ, &qBlock, portMAX_DELAY); // send the block to the SD Queue
        qBlock.len = 0;
      }
    }
  }
  else { // no 'len',  send what we already have remaining in the qBlock.
    Serial.println(F("No 'len'"));
    xQueueSend(SdQ, &qBlock, portMAX_DELAY); // send the block to the SD Queue
    qBlock.len = 0;
  }
  int ret = pdPASS;
  uint8_t err = NO_ERROR;
  while (uxQueueMessagesWaiting(SdError) && (ret == pdPASS)) {
    ret = xQueueReceive(SdError, &err, portMAX_DELAY);
    Serial.print(F("Error : "));
    Serial.println(err, DEC);
  }
  if (err != NO_ERROR) return false;
  return true; // success
}

bool InitSdCard() {
  if (hspi == NULL) {
    hspi = new SPIClass(HSPI);
  }
  hspi->begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, SD_CS);
  hspi->setFrequency(SPI_FRQ);

  if (!SD.begin(SD_CS, *hspi, SPI_FRQ)) {
    Serial.println(F("No SD card found"));
    return false;
  }
  return true;
}

//-----------------------------------------------------------------------------------

void HandleQueueToSD(void *pvParameters) {
  SdBlock taskBlock;
  char nameString[FILENAME_LENGTH + 1] = {'\0'};
  uint8_t taskState = WAIT_FOR_NAME;
  File sFile;

  for (;;) {

    vTaskDelay(1 / portTICK_PERIOD_MS);  // when running a higher priority

    if ((SdQ != NULL) && (SdFileName != NULL) && (SdError != NULL))  {  // Sanity check
                          // the Queues need to exist or we won't do anything                                           
      
      switch (taskState) {

        case WAIT_FOR_NAME: {
            if (nameString[0] == '\0') {  // only if we have no name

              if (uxQueueMessagesWaiting(SdQ)) {  // shouldn't happen, but we need to read them regardless
                int ret = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0);  // it should be there, if it isn't we can continue
                if (ret == pdPASS) {  // and send error messages back
                  uint8_t err = UNEXP_SDQ;
                  xQueueSend(SdError, &err, (TickType_t) 0); // don't wait
                }
                else if (ret == pdFALSE) {
                  uint8_t err = UNEXP_SDQ_EMPTY;
                  xQueueSend(SdError, &err, (TickType_t) 0); // don't wait
                }
              }

              if (uxQueueMessagesWaiting(SdFileName)) {
                int ret = xQueueReceive(SdFileName, &nameString, portMAX_DELAY);  // read the filename
                if (ret == pdPASS) {
                  if (nameString[0] == '\0') {
                    uint8_t err = UNEXP_FILENAME_NULL;
                    xQueueSend(SdError, &err, (TickType_t) 0);  // should not be empty
                  }
                  else {
                    sFile = SD.open(nameString, FILE_WRITE);  // open the file
                    if (!sFile) {
                      uint8_t err = FILE_NOT_CREATED;
                      xQueueSend(SdError, &err, (TickType_t) 0);  // error
                    }
                    else {
                      uint8_t err = NAME_RECEIVED;
                      xQueueSend(SdError, &err, (TickType_t) 0);  // No error !!
                      int ret = xQueueReceive(SdQ, &taskBlock, portMAX_DELAY);  // the first block should always be sent
                      if (ret == pdFALSE) {
                        uint8_t err = EMPTY_QUEUE;
                        xQueueSend(SdError, &err, (TickType_t) 0);
                        sFile.close();
                        nameString[0] = '\0';
                      }
                      else {
                        uint16_t bytesWritten = sFile.write(taskBlock.block, taskBlock.len);
                        if (bytesWritten != taskBlock.len) { // error
                          uint8_t err = WRITE_ERROR;
                          xQueueSend(SdError, &err, (TickType_t) 0);
                          nameString[0] = '\0';
                        }
                        else {
                          uint8_t err = HEADER_WRITTEN;  // no error
                          xQueueSend(SdError, &err, (TickType_t) 0);
                          taskState = WAIT_FOR_DATA;
                        }
                        sFile.close();  // close file anyway
                      }
                    }
                  }
                }
                else if (ret == pdFALSE) {
                  uint8_t err = UNEXP_FILENAME_EMPTY;
                  xQueueSend(SdError, &err, (TickType_t) 0); // don't wait
                }
              }
            }
          }

        case WAIT_FOR_DATA: {
            if (uxQueueMessagesWaiting(SdQ)) {
              sFile = SD.open(nameString, FILE_APPEND);  // open the file
              int ret = pdPASS;

              while (ret == pdPASS) {
                ret = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0); //portMAX_DELAY);
                if (ret == pdPASS) {
                  uint16_t bytesWritten = sFile.write(taskBlock.block, taskBlock.len);
                  if (bytesWritten != taskBlock.len) { // error
                    uint8_t err = WRITE_ERROR;
                    xQueueSend(SdError, &err, (TickType_t) 0); // send the error

                    // send specifics, the error was showing up until the data was processed
                    // Still a mystery.
                    err = bytesWritten >> 8;
                    xQueueSend(SdError, &err, (TickType_t) 0);
                    err = bytesWritten & 0xFF;
                    xQueueSend(SdError, &err, (TickType_t) 0);
                    err = taskBlock.len >> 8;
                    xQueueSend(SdError, &err, (TickType_t) 0);
                    err = taskBlock.len & 0xFF;
                    xQueueSend(SdError, &err, (TickType_t) 0);


                    nameString[0] = '\0';  // clear the name
                    taskState = WAIT_FOR_NAME;  // back to the name task
                    ret = pdFALSE; // Stop write loop
                    int fl = pdPASS;
                    while (fl == pdPASS) {  // flush the Queue
                      fl = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0);
                    }
                  }
                }
              } // read Queue loop
              sFile.close();
            }  // if SdQ available

            if (uxQueueMessagesWaiting(SdFileName)) {  // check if there is a name available
              int ret = xQueueReceive(SdFileName, &nameString, portMAX_DELAY);  // only then read the filename
              if (ret == pdPASS) {
                if (nameString[0] == '\0') {
                  uint8_t err = WRITE_STOP;
                  xQueueSend(SdError, &err, (TickType_t) 0);  // should be empty
                }
                else {   // we were in a stream, so the name shoyuld have been empty
                  uint8_t err = NAME_NOT_EXP;
                  xQueueSend(SdError, &err, (TickType_t) 0);  // not empty
                  nameString[0] = '\0';
                }
                taskState = WAIT_FOR_NAME; // task switches back either way
                int q = pdPASS;
                bool notEmpty = false;
                while (q == pdPASS) {  // flush the Queue just in case (should be empty)
                  q = xQueueReceive(SdQ, &taskBlock, (TickType_t) 0);
                  if (q == pdPASS) {
                    notEmpty = true;
                  }
                }
                if (notEmpty) { // the queue really should be empty
                  uint8_t err = Q_NOT_EMPTY;
                  xQueueSend(SdError, &err, (TickType_t) 0);  // should be empty
                }
              }
            }
          }  // wait for data

      }
    }  // Sanity check
  }  // Infinite loop
}

Who knows who else might want this, but i do !