Help with task structure when using DMA

I’m looking for some high-level help in how to structure my application that i’m porting to FreeRTOS on an STM32.

I have an IMU and Magnenmeter connected to the same i2c bus and two seperate interupt lines connected to the STM that signal when data is rady to be read.

The current baremetal flow is:

  1. GPIO ISR sets independants flags when data is either ready on the IMU or Mag
  2. Main loop checks flag and triggers a DMA read from the first device to trigger an interupt. I2C busy flag is also set, so that the “other device” is not attempted to be read until the first one finishes
  3. DMA complete ISR copies the read data to somewhere it can be later consumed. It also clears the i2c busy flag so that the “other” device can be DMAed in the same way

I want to move this over to FreeRTOS and initial thinking is to simply move the main loop over to a Task. However, is there a best practice for this type of use-case.

  1. Should i seperate out reading the IMU and Mag into two seperate tasks
  2. Flags are current globals, Some set from the ISRs others are from the main loop. Do I need to use mutexes and queues, if i know i’m only writing in the ISR and reading in the main loop for example ?
  3. Moving to FreeRTOS, is there any benifit to using the DMA and can I simplyfy the application by polling the i2c devices in a task. I assume whist the task is waiting, other tasks would be scheduled in anyway.

Appolgies if this the wrong place to ask.

Thanks Dilby.

The key thing to remember is you don’t want a task just looping checking a flag to decide if it has something to do, it should block on something, and the ISR (or other requestor) then signals that block that there is something to do, and then the task starts up. These blocks would be things like semaphores, queue, or the direct-to-task notification system.

I’d handle both sensors in 1 task owning the I2C bus. This avoids mutex-protecting the I2C bus accesses from multiple tasks.
You could use a queue of sensor ID items (probably just a byte) to signal the sensor being ready in its respective GPIO ISR to the task. But also task notification bits would be possible. A queue might be just easier to handle and to start with.
After getting notified with the ID of the sensor, the task then starts the corresponding DMA I2C (read) transaction and waits for the DMA getting completed.
DMA completion is also signaled from the DMA ISR to the task.
Here a simple task notification is fine or a binary semaphore, since you only want to wait for the DMA being finished.
After the task got signaled/unblocked you can post-process the received data as needed and repeat (waiting for the next sensor ID in the queue, which will be/was already signaled).
I think with this scheme you can properly serialize the I2C bus transactions without any additional and more complicated protection mechanism.

I suppose my experience is that the I2C bus is going to normally get eventually shared between things that don’t work in the same task, and it is simpler to mutex protect the I2C bus than write a separate ‘I2C Task’ to handle all the various I2C transactions for a project (the mutex also will allow granting of request in task priority order verse simple chronological from a service queue in the I2C Task. It also says that if you have multiple I2C Buses you don’t need a lot of rewriting if you move a device from one bus to another (My driver actually just encodes which bus as upper bits in the I2C device address bits, so moving busses is just changing the address of the device).

Thank you both the the great responses.

With the model of the a single task, would my task function look something like this:

uint8_t which_device = 0;

    for (; ;)
        xQueueReceive(handle, data, portMAX_DELAY);    //Block waiting to recieve INT for device
        which_device = data[0];
        if (which_device== 0x01)
            StartDMADevice1();
        else if (which_device == 0x02)
            StartDMADevice2();

        ulTaskNotifyTake( pdTRUE, portMAX_DELAY );    //Block waiting for DMA complete

        if (which_device== 0x01)
            handle_data_1();
        else if (which device == 0x02)
            handle_data_for device_2();
       
        which_device = 0;

Exactly :slight_smile: Great :+1:
I think you already got it right, but If you have a more complex say many-to-one (I2C peripheral) application then I’d also use the multi-tasking protected driver approach as Richard described. It allows better, probably simpler structuring of the application.

[Edited]

I’ve managed to split the reading of the IMU and Magnetometer into two tasks and used Mutexs to lock the i2c.

However, the suggestion from Richard has got me intrigued and I’m wondering what it would involve to implement this in a generic driver. I’ve made a start but struggling what to do with the the DMA handler.

i2x_read_mem can be called from any task, but once the dma_complete_handler is triggered, how can I determine which task to notify ?

void i2c_dma_complete_handler (void) {
    BaseType_t xHigherPriorityTaskWoken;
    xHigherPriorityTaskWoken = pdFALSE;
    vTaskNotifyGiveFromISR(???????, &xHigherPriorityTaskWoken);
    wTransferState = TRANSFER_COMPLETE;
   portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

void i2c_read_mem(uint8_t address, uint8_t reg_address, uint8_t len, uint8_t* data) {
	xSemaphoreTake(xi2c1Mutex, portMAX_DELAY);
	HAL_I2C_Mem_Read_DMA(&hi2c1, dev_address, reg_address, 1, data, len);
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY)
    xSemaphoreGive( xi2c1Mutex );
}

Basically with a generic driver model you have to provide a device context containing all things which are used in a generic way by the driver code but are different for each device. Often this is implemented as a struct which is setup for each device containing e.g. in your case the task handle, maybe the I2C address, etc.
In your implementation you could simply add a single global task handle variable (that’s the simplified device context) which is set in mutex protected code by the requesting task setting up the DMA transfer right before starting the DMA. The DMA ISR just uses this variable and you’ll be fine.
You could use xTaskGetCurrentTaskHandle to determine the task handle if it’s not already stored elsewhere.

1 Like

I use something like what Hartmut says, with basically a struct for each physical I2C bus, that has the mutex for that bus, and the needed variables, including a Semaphore for transfer complete (In actuality, this is often actually a C++ class). The i2c operations wait on the semaphore before returning to the caller. We could use the direct-to-task notification for the completion, but we often are also using that for other signals to the task, so the semaphore gets around problems of handling the other notifications that might come to the task.

We then use upper bits in the I2C Address word (use a 16 bit I2C address) to decide which device block to use, so most of the code just uses a constant for the I2C address and doesn’t need to know anything about multiple I2C busses.

1 Like

Thanks for Harmut, Richard. I’ve got something working along these lines. Good point about the semaphores. I am having intermittent issues, which I’m putting down to occasional interrupts coming in part way through the DMA transfer triggering a premature direct-to-task notification. I’ll do as you suggest and switch this out for a semaphore.

Thanks again.
Dilby.

You could try improve robustness against unwanted DMA ISR notifications by self-clearing the task handle variable (set it to NULL) in the ISR after signaling and adding a check if it’s valid or not before using it.
Using a semaphore won’t really help to deal with those spurious interrupts.
However, the best thing would be to improve the (DMA part of) the driver to avoid spurious/unwanted interrupts at all.

Sorry my reply wasn’t clear. The unwated ISR notifications are comming from the IMU signalling data is ready. In my particular application, I have a task notification waiting for the IMU INT to come in, to then trigger a a DMA i2c read, which in turn waits for a task notification for DMA completion. It’s the latter task notification that is triggered by an additional IMU INT comming in.

The cause of the issue was due to the sameple rate of the IMU being faster than I could read the i2c. Slowing it down significantly fixed the issue. However, I would like a more robust solution that can handle these cases. A semaphore would atleast block using a different method to the direct task notification (I think).

Thanks.

One big thing to watch out with direct-to-task notifications is that if you use them for multiple purposes, you need to take care that you don’t mis-interpret what each one means.

Computers do NOT like ambiguity.