3

I've been trying for a while now to transmit a block of data from my computer to an STM32L100C-DISCO over USART. For performance reasons, this is to be done using DMA. So far, however, I have not been able to get it to work. As I cannot seem to figure out what I might be doing wrong, I figured I'd ask here.

I'm using libopencm3, but unfortunately, their otherwise excellent repository of examples does not appear to contain one for DMA on the STM32L1xxx. I checked that I covered all the bases when it comes to the configuration options available in the common DMA header file, though.

Naturally, I've referred to the reference manual for the STM32L1xxx, which mentions the following requests table for DMA1, leading me to believe channel 6 is what I need to be using..

DMA requests table

As I was unsure about the size of the memory and peripheral (i.e. USART2), I varied across all combinations of 8, 16 and 32 bit for both, but to no avail.

Without further ado; this is a minimal working (well, not working..) excerpt of what I'm trying to do. I feel like I'm overlooking something in the DMA configuration, as USART by itself works fine.

At this point, anything is appreciated.

The idea behind this code is basically to loop forever until the data in the buffer is replaced entirely, and then when it is, output it. From the host, I'm sending a kilobyte of highly recognisable data, but all I'm getting back is malformed garbage. It is writing something, but not what I intend for it to write.

EDIT: Here's a picture of the memory map. USART2_BASE evaluates to 0x4000 4400, so that seems to be all right as well.

memory map

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include "libopencm3/stm32/usart.h"
#include <libopencm3/stm32/dma.h>

const int buflength = 1024;

uint8_t buffer[1024];

static void clock_setup(void)
{
    rcc_clock_setup_pll(&clock_config[CLOCK_VRANGE1_HSI_PLL_32MHZ]);
    rcc_peripheral_enable_clock(&RCC_AHBENR, RCC_AHBENR_GPIOAEN);
    rcc_peripheral_enable_clock(&RCC_APB1ENR, RCC_APB1ENR_USART2EN);
    rcc_periph_clock_enable(RCC_DMA1);

}

static void gpio_setup(void)
{
    gpio_mode_setup(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO3);
    gpio_mode_setup(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO2);
    gpio_set_af(GPIOA, GPIO_AF7, GPIO3);
    gpio_set_af(GPIOA, GPIO_AF7, GPIO2);
}

static void usart_setup(void)
{
    usart_set_baudrate(USART2, 115200);
    usart_set_databits(USART2, 8);
    usart_set_stopbits(USART2, USART_STOPBITS_1);
    usart_set_mode(USART2, USART_MODE_TX_RX);
    usart_set_parity(USART2, USART_PARITY_NONE);
    usart_set_flow_control(USART2, USART_FLOWCONTROL_NONE);

    usart_enable(USART2);
}

static void dma_setup(void)
{
    dma_channel_reset(DMA1, DMA_CHANNEL6);
    dma_set_priority(DMA1, DMA_CHANNEL6, DMA_CCR_PL_VERY_HIGH);
    dma_set_memory_size(DMA1, DMA_CHANNEL6, DMA_CCR_MSIZE_8BIT);
    dma_set_peripheral_size(DMA1, DMA_CHANNEL6, DMA_CCR_PSIZE_8BIT);
    dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL6);
    dma_disable_peripheral_increment_mode(DMA1, DMA_CHANNEL6);
    dma_enable_circular_mode(DMA1, DMA_CHANNEL6);
    dma_set_read_from_peripheral(DMA1, DMA_CHANNEL6);

    dma_disable_transfer_error_interrupt(DMA1, DMA_CHANNEL6);
    dma_disable_half_transfer_interrupt(DMA1, DMA_CHANNEL6);
    dma_disable_transfer_complete_interrupt(DMA1, DMA_CHANNEL6);

    dma_set_peripheral_address(DMA1, DMA_CHANNEL6, (uint32_t) USART2_BASE);
    dma_set_memory_address(DMA1, DMA_CHANNEL6, (uint32_t) buffer);
    dma_set_number_of_data(DMA1, DMA_CHANNEL6, buflength);

    dma_enable_channel(DMA1, DMA_CHANNEL6);
}

int main(void)
{
    int i;
    for (i = 0; i < buflength; i++) {
        buffer[i] = 65;
    }
    clock_setup();
    gpio_setup();
    usart_setup();
    dma_setup();

    usart_enable_rx_dma(USART2);
    char flag = 1;
    while (flag) {
        flag = 0;
        for (i = 0; i < buflength; i++) {
            if (buffer[i] == 65) {
                flag = 1;
            }
        }
    }
    usart_disable_rx_dma(USART2);

    for (i = 0; i < buflength; i++) {
        usart_send_blocking(USART2, buffer[i]);
    }
    usart_send_blocking(USART2, '\n');

    return 0;
}
Sam Protsenko
  • 14,045
  • 4
  • 59
  • 75
Joost
  • 4,094
  • 3
  • 27
  • 58
  • Just an idea: not a solution, but if you can time how long the RX takes and see if that's consistent with the baud rate (minimum of 0.08 seconds) that could show if the wrong event is triggering the DMA (one presumes the baud rate is correct since you have non-DMA working). – Weather Vane Jun 29 '15 at 16:28
  • I'm not sure I fully understand what you meant, but I tried lowering the baud rate to 9600 on both sides and that did not fix the problem (or present me with meaningful output). Calls to `usart_send_blocking` and `usart_recv_blocking` do indeed work just fine. – Joost Jun 30 '15 at 07:57
  • To add; the 9600 was chosen to err on the side of caution – I figured that it would be a safe lower-bound. – Joost Jun 30 '15 at 08:06
  • My point was only that if the 1024 chars were received by `main()` in less time than they should, then the DMA is being wrongly triggered. If they arrive in the correct time interval, then the DMA is probably being correctly triggered, but the data not being correctly read from the UART, or correctly written to memory. Just a matter of elimination. – Weather Vane Jun 30 '15 at 08:14
  • It is precisely this process of elimination that I'm having difficulties with to perform. How would you suggest testing if the data is correctly read, but not written to memory properly? I can only imagine being at the end of the line to test if the contents of the `buffer` are a-OK, but not where to go in between.. – Joost Jun 30 '15 at 09:31
  • I should note that the content of `buffer` (i.e. the 'garbage') is fairly identical for each run, for different input data. Looks like uninitialised memory. If I do not enable DMA, though, and just let it be (after initialisation to `65...65`), it does behave predictably by outputting a sequence of `65`s. – Joost Jun 30 '15 at 11:43
  • I don't know anything about this apart from what I read: but from your posted datasheet table, how does the DMA function know to hook up with `USART2RX` and not `I2C1_TX`? My other naive question is why you have `dma_set_peripheral_address(DMA1, DMA_CHANNEL6, (uint32_t) USART2_BASE);` and not its `RXD` register? – Weather Vane Jun 30 '15 at 20:11
  • I feel like your two questions are related; it does not know it is reading from USART2, but it knows it's reading from the USART2 address. In the memory map, there do not appear to be distinct addresses for TX and RX: only this base address is specified. – Joost Jul 01 '15 at 07:35
  • 1
    The data sheet I am looking at page 726 says. *"Write the USART_DR register address in the DMA control register to configure it as the source of the transfer."* You have used `USART_BASE`. http://www.st.com/web/en/resource/technical/document/reference_manual/CD00240193.pdf – Weather Vane Jul 01 '15 at 08:12
  • 1
    USART2 is mapped to 0x40004400 and USART_DR has an offset of 4. – Weather Vane Jul 01 '15 at 08:33
  • Ah, thanks! `USART2_BASE` evaluates to `0x40004400`, indeed.. How should I incorporate that offset? I tried adding it to the address, but that did not seem to have helped. – Joost Jul 01 '15 at 08:37
  • Hm. Where have you done this: *"DMA mode can be enabled for reception by setting the DMAR bit in USART_CR3 register."*? – Weather Vane Jul 01 '15 at 08:41
  • That happens in the call to `usart_enable_rx_dma`, apparently. https://github.com/libopencm3/libopencm3/blob/3a106dbd10dfde1a1da16692f6949b455b549235/lib/stm32/common/usart_common_all.c#L248 – Joost Jul 01 '15 at 08:50

2 Answers2

4

I am not familiar with libopencm3 or the STM32L series, but I am familiar with the STM32F series. I know there are differences in peripherals, but I believe your mistake lies in the following line:

dma_set_peripheral_address(DMA1, DMA_CHANNEL6, (uint32_t) USART2_BASE);

Here you are setting your DMA peripheral address to the USART2_BASE address, but typically, you would want to set this to the USART2 data register, which may not be right at USART2_BASE.

I see now that some of the comments on your question have pointed this out, but there is still a remaining question of how to indicate the data register. With the ST peripheral library, there are memory mapped structures for the peripherals. It appears that libopencm3 has a defined macro you can use for the data register address: USART2_DR. Here is the definition in the documentation

So, I believe if you change the line above to the following, it may solve your problem:

dma_set_peripheral_address(DMA1, DMA_CHANNEL6, (uint32_t) USART2_DR);
rjp
  • 1,760
  • 13
  • 15
  • Hmm, it appears I was wrong in interpreting `USART2_BASE` to be the only one available. After installing libopencm3 properly instead of using a (what now turns out to be a) small subset of the available files, `USART2_DR` does resolve. Instead of garbage, I now get the last character received via `usart_recv_blocking`, repeated. It's not precisely where I want to be (and I'm probably still miss-configuring something minor), but it's getting close. – Joost Jul 02 '15 at 12:45
  • It's been a while since I've used the STM32 DMA controller, but it sounds like, for some reason, that the USART is not properly controlling the flow on the DMA channel, and the DMA controller is just reading out `datasize` bytes from the `USART2_DR` at its own pace, rather than the USART's. Again, popping back to the libopencm3 documentation, it looks like `dma_set_peripheral_flow_control` is the call to set the DMA controller to be paced out by the USART. http://libopencm3.github.io/docs/latest/stm32l1/html/group__dma__file.html#gaf667ccb9a78c8fe76f2cf256fa153b6b – rjp Jul 08 '15 at 20:04
  • Unfortunately, flow control requires additional hardware that does not work out in my current situation (i.e. my console does not have RTS/CTS pins) – Joost Jul 10 '15 at 09:57
  • This isn't flow control of the UART, but flow control of the DMA transfer. Right now your transfer is setup to allow the DMA controller to control the flow from the UART to memory, but the DMA controller doesn't know when there's a new character, so it just repeatedly copies the DR over and over (which is why you get the last character received repeated). If you add the call that I mentioned, it allows the UART to control the flow of the DMA. When a new character is received, it sends a signal to the DMA controller which tells it to read from the source and copy to the destination. – rjp Jul 10 '15 at 13:55
1

In the end, this is the configuration that I used to get it working.

const int datasize = 32;

char buffer[32];

static void dma_setup(void)
{
    dma_channel_reset(DMA1, DMA_CHANNEL6);

    nvic_enable_irq(NVIC_DMA1_CHANNEL6_IRQ);

    // USART2_DR (not USART2_BASE) is where the data will be received
    dma_set_peripheral_address(DMA1, DMA_CHANNEL6, (uint32_t) &USART2_DR);
    dma_set_read_from_peripheral(DMA1, DMA_CHANNEL6);

    // should be 8 bit for USART2 as well as for the STM32L1
    dma_set_peripheral_size(DMA1, DMA_CHANNEL6, DMA_CCR_PSIZE_8BIT);
    dma_set_memory_size(DMA1, DMA_CHANNEL6, DMA_CCR_MSIZE_8BIT);

    dma_set_priority(DMA1, DMA_CHANNEL6, DMA_CCR_PL_VERY_HIGH);

    // should be disabled for USART2, but varies for other peripherals
    dma_disable_peripheral_increment_mode(DMA1, DMA_CHANNEL6);
    // should be enabled, otherwise buffer[0] is overwritten
    dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL6);

    dma_set_memory_address(DMA1, DMA_CHANNEL6, (uint32_t) &buffer);
    dma_set_number_of_data(DMA1, DMA_CHANNEL6, datasize);

    dma_disable_transfer_error_interrupt(DMA1, DMA_CHANNEL6);
    dma_disable_half_transfer_interrupt(DMA1, DMA_CHANNEL6);
    dma_enable_transfer_complete_interrupt(DMA1, DMA_CHANNEL6);

    usart_enable_rx_dma(USART2);
    dma_enable_channel(DMA1, DMA_CHANNEL6);
}

Then, when the transfer is complete, the override of the dma1_channel6_isr function gets called and all the data is in buffer.

I have submitted the full working code as a pull request to the libopencm3-example repository. You can find it here. I will make sure to update the link when the code gets merged.

Joost
  • 4,094
  • 3
  • 27
  • 58
  • As a suggestion for enhancing the example, and something that is often lacking when it comes to asynchronous DMA, would be to support a receive timeout. Some USART peripherals have a timeout interrupt that can be generated internally, and on some parts you'll need to use a timer interrupt. With the configuration you have above, and I know this is what you were going for, if 31 characters are received, nothing will be echoed. Using a receive timeout, 31 characters would be echoed if no further characters are received, and then it could reconfigure and receive up to the next 32 characters. – rjp Jul 08 '15 at 20:09
  • That's not applicable to my current situation (as missing a single byte is a reason for termination), but I could include it in the example. Thanks for the suggestion. – Joost Jul 10 '15 at 09:58