Search code examples
stm32dmausart

STM32F446 USART in DMA mode only transmitting once


I'm learning to use DMA on a STM32F446 and tried to send data over USART. The goal is to do some calculations and send the results to a PC via RS232. Here is my MWE:

#include <stdint.h>
#include <stdio.h>
#include "stm32f446xx.h"

#define BAUDRATE ( 9600 )
#define USART2_TX_PIN       (2)
#define USART2_RX_PIN       (3)

int main(void) {    

    volatile uint32_t core_clock_hz = 16000000;
    uint16_t uartdiv = core_clock_hz / BAUDRATE;
    uint8_t TX_buffer[2];
    TX_buffer[0] = 48; // this is a "0" in ASCII
    TX_buffer[1] = 49; // this is a "1" in ASCII
    
    // configure peripheral clocks
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; // Enable the SYSCFG peripheral
    RCC->APB1ENR  |= ( RCC_APB1ENR_USART2EN ); // Enable peripheral clocks:  USART2
    RCC->AHB1ENR  |= ( RCC_AHB1ENR_GPIOAEN ); // Enable peripheral clocks: GPIOA    
    RCC->AHB1ENR  |= ( RCC_AHB1ENR_DMA1EN ); // Enable peripheral clock: DMA1
    
    // Configure pins A2 (TX), A3 (RX) for USART2. TX: alternate out push-pull, RX: in floating
    // TX
    GPIOA->MODER    &= ~(0x3 << (USART2_TX_PIN*2));     // reset all bits
    GPIOA->MODER    |=  (0x2 << (USART2_TX_PIN*2));     // 10 = alternate
    GPIOA->OSPEEDR  &= ~(0x3 << (USART2_TX_PIN*2));     // reset all bits
    GPIOA->OSPEEDR  |=  (0x0 << (USART2_TX_PIN*2));     // 00 = low speed
    GPIOA->OTYPER   &= ~(0x1 <<  USART2_TX_PIN);        // 0 = push-pull
    GPIOA->PUPDR    &= ~(0x3 << (USART2_TX_PIN*2));     // 00 = no pull-up / pull-down
    // RX
    GPIOA->MODER    &= ~(0x3 << (USART2_RX_PIN*2));     // reset all bits
    GPIOA->MODER    |=  (0x2 << (USART2_RX_PIN*2));     // 10 = alternate
    GPIOA->PUPDR    &= ~(0x3 << (USART2_RX_PIN*2));     // reset all bits
    GPIOA->PUPDR    |=  (0x0 << (USART2_RX_PIN*2));     // 00 = no pull-up / pull-down , 01 = pull-up
    // set alternate pin function AF7 for PA2 and PA3. AFR[0] = AFRL
    GPIOA->AFR[0]   &= ~(0xF << USART2_TX_PIN*4);       // clear all bits
    GPIOA->AFR[0]   |=  (0x7 << USART2_TX_PIN*4);       // set AF7
    GPIOA->AFR[0]   &= ~(0xF << USART2_RX_PIN*4);       // clear all bits
    GPIOA->AFR[0]   |=  (0x7 << USART2_RX_PIN*4);       // set AF7
    
    USART2->BRR = ( ( ( uartdiv / 16 ) << USART_BRR_DIV_Mantissa_Pos ) | ( ( uartdiv % 16 ) << USART_BRR_DIV_Fraction_Pos ) ); // configure USART baudrate
    USART2->CR1 |= ( USART_CR1_RE | USART_CR1_TE | USART_CR1_UE ); // Enable the USART peripheral

// Main loop
while ( 1 ) {
    
    DMA1_Stream6->CR &= ~(DMA_SxCR_EN); // deactivate DMA stream for configuration
    DMA1_Stream6->CR &= ~(DMA_SxCR_CHSEL); // clear bits
    DMA1_Stream6->CR |=  (DMA_SxCR_CHSEL_2); // 100 = channel 4
    DMA1_Stream6->CR &= ~(DMA_SxCR_PL); // priority 00 = low
    DMA1_Stream6->CR &= ~(DMA_SxCR_PSIZE); // size 00 = 8bit
    DMA1_Stream6->CR |= (DMA_SxCR_MINC); // increment memory pointer with each DMA transfer     
    DMA1_Stream6->CR &= ~(DMA_SxCR_DIR); // clear bits
    DMA1_Stream6->CR |=  (DMA_SxCR_DIR_0); // 01 = memory-to-peripheral
    DMA1_Stream6->PAR = ( uint32_t )&USART2->DR; // peripheral memory address
    DMA1_Stream6->M0AR = ( uint32_t )&TX_buffer; // data memory address
    DMA1_Stream6->NDTR = ( uint16_t ) 2; // number of bytes to transfer
    
    DMA1->HISR &= ~(DMA_HISR_TCIF6 | DMA_HISR_HTIF6 | DMA_HISR_TEIF6 | DMA_HISR_DMEIF6 | DMA_HISR_FEIF6); // clear DMA flags
    USART2->SR &= ~(USART_SR_TC); // clear USART transfer complete flag
    DMA1_Stream6->CR |= (DMA_SxCR_EN); // set EN bit to activate DMA stream
    
    // does not help: USART2->CR1 |= ( USART_CR1_RE | USART_CR1_TE | USART_CR1_UE ); // Enable the USART peripheral
    USART2->CR3 |= (USART_CR3_DMAT); // enable USART DMA mode
    
    // wait for end of transfer
    while ( !(DMA1->HISR && DMA_HISR_TCIF6) ) {}
    while ( !(USART2->SR && USART_SR_TC) ) {} 
    
    //
    // do calculations here, modify TX_buffer for next transfer cycle
    //
    
} // while (1)
} // main

The code should send the data in TX_buffer in an endless loop, thus I was expecting to receive a sequence of 01010101... in the PC's terminal. However, I only get a single 01 and then the transmission stops. As data is generally sent, GPIOs, clocks, ... seem to be configured correctly.

I guess, after one loop cycle of the while(1), the DMA or the USART are not reset to a state where they accept new transfers, but I couldn't figure out what exactly is missing. I already thought about missing ISR routines and IRQs. Many examples on the net use them, but I could not find any functionality in them which is not already in my main loop. Thus, my MWE does not use any interrupts or interrupt routines. All interrupts are deactivated in the DMA configuration register.

In circular mode of the DMA, the endless transmission is working, but this seems not to be the appropriate solution for my scenario of calculate -> send > calculate -> send -> ...

How do USART and DMA have to be configured in this case to allow multiple subsequent transmissions?

EDIT:

Added a compileable MWE. Additional information which might be helpful: There are also no interrupts configured for the USART. My compiler options are:

CFLAGS += -mcpu=$(MCU_SPEC)
CFLAGS += -mthumb
CFLAGS += -Wall
# (Set error messages to appear on a single line.)
CFLAGS += -fmessage-length=0
CFLAGS += --specs=nosys.specs
CFLAGS += -ffunction-sections
CFLAGS += -fdata-sections
CFLAGS += -lm
# (Custom flags sent to the compiler)
CFLAGS += -D$(ST_MCU_DEF)
CFLAGS += -DVVC_$(MCU_CLASS)
#CFLAGS += -DVVC_$(MCU)
# FPU config
ifeq ($(MCU_CLASS), $(filter $(MCU_CLASS), L4 G4 WB F4))
    CFLAGS += -mhard-float
    CFLAGS += -mfloat-abi=hard
    CFLAGS += -mfpu=fpv4-sp-d16
else
    CFLAGS += -msoft-float
    CFLAGS += -mfloat-abi=soft
endif

Solution

  • Let's look at the reference manual of the MCU together. You are not clearing the flags of DMA.

    DMA1->HISR &= ~(DMA_HISR_TCIF6 | DMA_HISR_HTIF6 | DMA_HISR_TEIF6 | DMA_HISR_DMEIF6 | DMA_HISR_FEIF6); // clear DMA flags
    USART2->SR &= ~(USART_SR_TC); // clear USART transfer complete flag
    

    One of these lines works, the other one doesn't do anything, because

    enter image description here

    enter image description here

    USART SR TC bit says it's rc_w0, while DMA's HISR is all "r" - read only bits. Writing to that register doesn't do anything. You need to use dedicated clear flag register of DMA.

    enter image description here

    So instead, this should work (the register is write-only):

    DMA1->HIFCR = DMA_HIFCR_CTCIF6 | DMA_HIFCR_CHTIF6 | DMA_HIFCR_CTEIF6 | DMA_HIFCR_CDMEIF6 | DMA_HIFCR_CFEIF6; // clear DMA flags
    

    Notice I'm not using |=, because |= will mean that we need to read the register first (like x |= y means x = x | y), and the register is not readable. So you prepare value for it and write it straight there without reading anything from it.