Using I2C and DMA on STM32L412

I want to use an external four-channel I2C DAC for some volume control duties in a project. I want this to run continuously, starting a fresh transfer as soon as the old one is finished. Im trying to follow the implementation of the HAL command HAL_I2C_Master_Transmit_DMA - this works in my code but only produces a single conversion.

How can i set I2C / DMA to work continuously? Do i have to use a timer to schedule conversions?

I don’t think it’s strictly necessary for me to use the HAL at all, as the program is very simple its much faster for me to write to registers directly, im just using it to try and reverse engineer the code to see what registers I have to set but anything that gets this working continuously is fine

I think you should be able to set up a timer to generate an event that triggers the DMA to transfer data from memory to the I2C peripheral. I’d try to keep it simple and just use a basic timer, rather than a general purpose or advanced timer. On STM32f4 I think TIM6 and TIM7 are basic timers but sometimes only one of the two can trigger an update event/request for the DMA controller

A good reference might be this project on Deep Blue Embedded: https://deepbluembedded.com/stm32-dac-sine-wave-generation-stm32-dac-dma-timer-example/

In this example above, the DMA is being triggered by a timer to transfer data to the DAC peripheral. You could do something very similar with the I2C peripheral. I think you should be able to configure most, if not all, of the code using STM32CubeMX.

Let me know if this helps or if you found a different solution!

Following up on this - the deep blue embedded example uses a characteristic of the DAC peripheral: it can be directly triggered by a timer’s update event.

I don’t think the DMA can easily be configured to generate a transfer from memory to peripheral when a timer reaches the ARR value. It seems like there is a way - there is a discussion here on stackover flow that implies it’s possible, but I haven’t dug into it enough to fully understand this:

An easier way to do this might be to have the timer to generate an interrupt every TBD msecs and during that interrupt you could trigger a DMA transfer from memory to the I2C peripheral (calling the same function you originally mentioned… HAL_I2C_Master_Transmit_DMA). The interrupt handling may still create some overhead but it would free up the processor while the data is transferred.

Seems like this topic was abandoned, but I think I know what he was after…
.

Here’s the function he mentioned:
.
.

HAL_I2C_Master_Transmit_DMA

Function name
HAL_StatusTypeDef HAL_I2C_Master_Transmit_DMA (I2C_HandleTypeDef * hi2c, uint16_t DevAddress,
uint8_t * pData, uint16_t Size)

Function description
Transmit in master mode an amount of data in non-blocking mode with DMA.

Parameters
• hi2c: Pointer to a I2C_HandleTypeDef structure that contains the configuration information for the specified
I2C.
• DevAddress: Target device address: The device 7 bits address value in
datasheet must be shifted to the
left before calling the interface
• pData: Pointer to data buffer
• Size: Amount of data to be sent

.
.
.

And I’m pretty sure the 2nd half of using that function is this:
.
.

HAL_I2C_MasterTxCpltCallback

Function name
void HAL_I2C_MasterTxCpltCallback (I2C_HandleTypeDef * hi2c)

Function description
Master Tx Transfer completed callback.

Parameters
• hi2c: Pointer to a I2C_HandleTypeDef structure that contains the configuration information for the specified
I2C.

Return values
• None:
.
.
.

That 2nd function is what the DMA interrupt is going to look at for user code. Code that will usually tell it what to do to refill the buffer it reads.
Like this for I2S from my synth project:

void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s) {
make_sound((uint16_t *) (audiobuff + BUFF_LEN_DIV2), BUFF_LEN_DIV4);
}

When the DMA gets to the end of “audiobuff”, which is an array[ ], this function will get called. Then it calls another function, “make_sound()”, that will refill the buffer.

Pretty sure that’s what he was after. The first function initializes the DMA stream to begin. But without any callback function, it will only stream out the buffer one time and quit.

The function I showed example of with I2S actually has TWO callbacks. A HALF CALLBACK and a COMPLETE CALLBACK.
When the half callback is called, it will execute code that will refill the first half of the buffer (what we just streamed out) while DMA is streaming the last half of the buffer.
The Complete Callback will refill the last half of the buffer while DMA streams out the first half.

2 Likes

Ah! I misread this detail: “starting a fresh transfer as soon as the old one is finished.”

I was assuming the original post was asking about outputting data at a specific rate, which is why I recommended a timer.

I think your approach would work if you just want to start another transfer immediately after the old one finished… but wouldn’t it be hard to predict the output sample rate?

I don’t think the I2C peripheral clocks out data in a nice periodic way like the I2S peripheral. I think there might be some jitter and timing weirdness in the output data.