Okay, now, let’s implement a WS2812-controller using an STM32F103 microcontroller using DMA transfers. Again, we have a HAL implementation and a direct implementation. Basically, I switched to the direct implementation because I couldn’t find something in the HAL implementation, but I found it later.
Memory considerations. The example from the HAL uses 32 bit values. As I only need low values, 8 bit values would be fine, and needed to minimise the memory usage. As I couldn’t find the option to use 8 bit values, I switched to a direct implementation.
Now… the option is there… but I was looking where it was set to 32. So, I was looking for something mentioning 32. However, it’s called
hdma_tim.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD ;
It didn’t occur to me to seach for WORD
to represent uint32_t
. Basically, to me, WORD
is a system-specific thing. Different platforms have different word sizes, so using WORD to indicate a 32 bit int doesn’t come natural to me.
Nevertheless, I continued with my direct implementation. I suppose, this way I get to know the hardware, and I am doing some things I believe are not available in the HAL, or they’re hidden so well, that it’s easier to find them in the datasheet.
So, there is a DMA controller. Bascially, I point to to a block of memory and a peripheral. The peripheral requests the next unit of data when it’s ready to process it. That’s basically how it works.
The DMA controller has channels. One thing to be aware of, each peripheral is associated with a certain channel. You have to use the correct channel or it won’t work.
So, let’s have a look at this DMA controller. To use the DMA Controller with Timer 2, we need DMA Controller 1, Channel 2. We need to associate the DMAR register of the Timer to the DMA Channel. The timer peripheral uses 16 bit values. As we have low values and want to conserve memory, the data buffer is 8 bit.
DMA1_Channel2->CPAR = &(TIM2->DMAR); // DMA 1 Channel 2 to TIM2 DMA1_Channel2->CCR = 0x00; DMA1_Channel2->CCR |= (0x01 << DMA_CCR_PSIZE_Pos); // Peripheral size 16 bit DMA1_Channel2->CCR |= (0x00 << DMA_CCR_MSIZE_Pos); // Memory size 8 bit DMA1_Channel2->CCR |= (0x1 << DMA_CCR_DIR_Pos); // Memory to Peripheral DMA1_Channel2->CCR |= (0x1 << DMA_CCR_MINC_Pos); // Memory increasement DMA1_Channel2->CCR |= (0x0 << DMA_CCR_PINC_Pos); // Peripheral increasement DMA1_Channel2->CCR |= (0x0 << DMA_CCR_CIRC_Pos); // Circular mode DMA1_Channel2->CCR |= DMA_CCR_TCIE; // Enable transfer complete interrupt
Now, let’s look at the timer. Here, we say where the data should go that is offered by a DMA tranfer. We set it to go to the CCR1 register. The compare register that sets the PWM period. Each timer has 4 channels, and 4 of those registers. Here, I set the timer to receive 4 transfers at a time. This way, I output to all 4 channels at the same time. Furthermore, I have to enable the Update DMA request.
TIM2->DCR |= (( 12 ) << TIM_DCR_DBA_Pos); // DMA Transfer Base address CCR1 TIM2->DCR |= (( 3 ) << TIM_DCR_DBL_Pos); // 4 Transfer at a time (CCR1 to CCR4) TIM2->DIER |= TIM_DIER_UDE; // Update DMA Request Enable
When this is set up, a DMA transfer can be initiated by
DMA1_Channel2->CNDTR = size; DMA1_Channel2->CMAR = memory; TIM2->CCMR1 |= 1; // enable timer DMA1_Channel2->CCR |= 1; // Enable DMA TIM2->EGR = TIM_EGR_UG;
Point to the memory block, set the length of the block, enable timer, enable DMA, and finally, let the timer request an update from the DMA controller. Now, each period the timer will send an update request to the DMA controller, and this way, we can control four LED strips simultaneously. And this is what mentioned before, I couldn’t find an option in the HAL to control multiple channels simultaneously.