rogerclarkmelbourne / Arduino_STM32

Arduino STM32. Hardware files to support STM32 boards, on Arduino IDE 1.8.x including LeafLabs Maple and other generic STM32F103 boards
Other
2.54k stars 1.26k forks source link

STM32F103 DMA for USART Read Setup #662

Closed rsonnicksen closed 5 years ago

rsonnicksen commented 5 years ago

I would like to learn how to utilize the DMA feature of the STM32F103 to reduce the number of interrupts needed to handle an incoming text stream from a GPS receiver. I'm getting approx 6,000 characters per second from my GPS receiver so that's 6,000 interrupts per second, which is my motivation for using DMA, in addition to simply learning how to implement this powerful feature of the STM32 family. I am having some very basic problems configuring the DMA.

The first problem is that I can't seem to be able to write to DMA1_BASE->CMAR or CPAR. In the Reference Manual I'm instructed to disable the dma channel (6) which I am doing with DMA1_BASE->CCR6 = 0;

Then I write to CMAR and CPAR, but when I print them out using Serial.println((uint32_t)DMA1_BASE->CMAR6,HEX); I get 0. What am I doing wrong?

The second issue I have is that I'm not clear how to name the dma1_channel6 interrupt service routine. I've looked through the header and .c files for these but can't find them, so even if I get the CMAR and CPAR written, I won't be able to handle the DMA interrupt. I was thinking I could write the address of my ISR to the NVIC vector table but I haven't had luck with that either. I'm not aware of an "attach_interrupt" function for the DMA channels which allows me to define the ISR routine.

This is the output from my code:

000000000000000000000000000000000000000000000000000000000000 0 20000824 0 40004404

This is my code:

GPS_DMA_test.txt

stevstrong commented 5 years ago

You forgot to switch on the DMA clock. I am still of opinion that it is an overkill to use DMA for serial data. However, you can find a nice implementation of a DMA transfer using libmaple functions in the SPI library: https://github.com/rogerclarkmelbourne/Arduino_STM32/blob/master/STM32F1/libraries/SPI/src/SPI.cpp

rsonnicksen commented 5 years ago

@stevstrong , Thanks Steve. I was able to turn on the DMA clock using: dma_init(DMA1);
which is all that function does. It also clued me in that when manually configuring the USART, I need to also turn on the clock for the USART which I did using: rcc_clk_enable(USART2->clk_id);

Here is the code I used to get the DMA to read the USART2:

GPS_DMA_test2.txt

My next issue is how to get the USART interrupt to work. Obviously somewhere in the Library there is an interrupt handler that is reading each character from the USART and putting it in a ring_buffer, but I don't fully understand how it is working. I found the routine below in C:\Program Files (x86)\Arduino\hardware\Arduino_STM32-master\STM32F1\cores\maple\libmaple\usart_f1.c

weak void irq_usart2(void) { usart_irq(&usart2_rb, &usart2_wb, USART2_BASE); }

I believe the weak attribute means that I can write my own irq_usart2 routine, and my definition will supercede the library routine. So that's what I did, but when I run my code and enable the USART2 interrupt, my code seems to lock-up. I don't see an "usart_attach_interrupt" function which allows me to define my interrupt routine for usarts, like what is available for dma.

How should I specify the USART2 irq handler?? (PS - I don't want to modify the library files)

rsonnicksen commented 5 years ago

@stevstrong , Oh, I forgot to mention, that the reason I want the USART interrupt, is that I'm going to use the IDLE interrupt to tell me when I'm done receiving data (the GPS module transmits bursts of approx 350 characters 20x per second). The length of the data is unknown which is why I want to use the IDLE interrupt to tell me when the transmission is done, then I will go and parse the data I've received to extract the information I need. Using this method, I won't use the DMA interrupts. I will simply make sure my buffer is big enough to hold the largest burst of data, and then reset the DMA transfer between bursts, so that it always starts at the beginning of the buffer.

I will disable the RXNEIE interrupt so that my irq_handler is only called on the IDLE condition, not called each time a character is received.

stevstrong commented 5 years ago

When you overwrite the uasrt ISR, then make sure that you keep the call of the original function of the ISR, and add your own function to that. Do not do Serial output within the ISR! Is the IDLE irq a feature of the USART interface? Or is it an external signal from the GPS module? If it is an external signal then you can detect that by attaching a EXT interrupt to an input pin (look for the function attachInterrupt(...))

rsonnicksen commented 5 years ago

@stevstrong <<When you overwrite the uasrt ISR, then make sure that you keep the call of the original function of the ISR, and add your own function to that.>> Not sure I understand your comment. The LibMaple library has routine weak void irq_usart2(void) {... which I am not changing. I am adding my OWN routine __irq_usart2(void) without the "__weak" attribute, thinking mine will supercede the library routine. If that is the case, I should be able to anything I want inside the irq. Not sure what you meant by keep the call of the original function of the ISR

I know that I "shouldn't" call a serial print from the IRQ, but I'm just trying to detect if the irq routine is being called. I can use a counter++ in the irq instead. This is just a test program to get it working.

The IDLE irq IS a native function of the USART in the STM32F103. The IDLE flag turns on if the Rx line stays high for the equivalent of 1 byte. The IDLE flag is read from the usart status register, and this flag can generate a USART interrupt if enabled. No external interrupts required. So now, I'm really just trying to understand how the USART IRQ handler is specified, and why my program appears to lock-up when I enable this USART interrupt.

stevstrong commented 5 years ago

I mean something like this:

void __irq_usart2(void) { // this is not weak anymore !!!
  myOwnSuperISR();  // add this to do your business
  usart_irq(&usart2_rb, &usart2_wb, USART2_BASE); // keep old function call, this clears some IRQ flags
}

I don't know how to handle the IDLE interrupt, never experienced wit that.

I still consider using DMA for processing serial data an overkill.

rsonnicksen commented 5 years ago

OK, thanks for clarifying.

The code in the usart_irq function in the library only handles the RXNE and TXE conditions, and I will have these interrupts turned OFF so that this code DOES NOT alter the usart state. So in my specific case, I will not include this in my irq handler, but yes, in the general case, that would be the direction to go so that the default functionality was not lost.

When I finally get this working, I will do some speed testing to see the difference between interrupt driven usart, and DMA based usart. Thanks for your help.

stevstrong commented 5 years ago

You will not notice any significant difference because the UART speed is very low compared to, e.g, SPI. How much is the Baudrate of the serial data from GPS? 9600?

rsonnicksen commented 5 years ago

I'm getting 20 messages per second, each around 300 characters. I've got the baud rate up to 115200 with no errors, but I'm also in an electrically quiet environment, which will change when I install this near BLDC motors. I did finally get the USART interrupt to work. It turned out my irq_usart2 routine wasn't being recognized by the compiler because it's apparently considered a C++ declaration, so I changed from: void __irq_usart2(void){... to: extern "C" void irq_usart2(void){.. and that seemed to make it work. I found that info in: https://github.com/rogerclarkmelbourne/Arduino_STM32/wiki/Libmaple-interrupt-handlers

Now I just need to tie it all together. Thanks again for your help.

rsonnicksen commented 5 years ago

OK, Here is my final code. I don't have the parser working yet, but I am receiving all of the gps data and am able to print it out.

I hope this helps somebody, because it certainly wasn't "easy" to get this working.

//This file uses the USART IDLE interrupt to trigger when to read the GPS data
//which is received from the USART via DMA (Direct Memory Access)
// Need to review to see how many of these #include are not necessary 
#include <string.h>
#include <series/dma.h>
#include <libmaple/dma.h>
#include <dma.h>
/* <libmaple/dma_common.h> buys us dma_dev and other necessities. */
#include <libmaple/dma_common.h>
#include <libmaple/libmaple_types.h>
#include <libmaple/usart.h>
#include <usart_private.h>
#include <series/usart.h>

#include <libmaple/nvic.h>
#include "dma_private.h"

extern "C"  void __irq_usart2(void);
 void find_gps();
 void gps_setup();
 void gps_dma_setup();

//=========================  DMA RELATED VARIABLES  =======================================
#define GPS_PACKET_SIZE (800)
static char gps_read_buffer[GPS_PACKET_SIZE];
uint32_t dma_mode;
uint32_t snap1, snap2, snap3;
uint16_t i;

volatile bool gps_data_available;
volatile int gps_data_len;
volatile uint32_t temp_var;
volatile bool idle_now = false; 
volatile bool idle_prev = false;
volatile uint32_t usart_count_irq, usart_count_irq_prev, loop_usart_count;
bool gps_exists;

#define DEBUG

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Setup routine
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
  // put your setup code here, to run once:
  Serial.begin(57600);
  delay(5000);
  #ifdef DEBUG
  Serial.println("Begin");
  #endif

  find_gps();
  if(gps_exists){
    gps_setup();
    gps_dma_setup();

    #ifdef DEBUG
    Serial.println("Setup done");
    #endif

    nvic_irq_enable(NVIC_USART2); 
    USART2_BASE->CR1 |= USART_CR1_IDLEIE;
  }
}

//----------------------------------------------------   MAIN LOOP ---------------------------------------------------------------------
void loop() {
  delayMicroseconds(3500);

  if(gps_data_available){
    //gps_parse_data(gps_data_len);
    Serial.println(gps_read_buffer);
    gps_data_available = false;
  }
}

//------------------------  DMA ISR HANDLER  ------------------------------------------------
//This routine can be used when circular transfers are enabled
void dma1_channel6_isr(void) {
  #ifdef DEBUG
  Serial.println("dma_isr");
  #endif

  if (DMA1_BASE->ISR & DMA_ISR_HTIF6){  //Half Transfer
    DMA1_BASE->IFCR |= DMA_ISR_HTIF6; //Clear the flag
    //  Process the first half of the memory buffer
    #ifdef DEBUG
    Serial.println("Half-Transfer");
    #endif
  }
  if (DMA1_BASE->ISR & DMA_ISR_TCIF6){  //Transfer Complete
    DMA1_BASE->IFCR |= DMA_ISR_TCIF6; //Clear the flag
    //  Process the second half of the memory buffer
    #ifdef DEBUG
    Serial.println("Transfer-Complete");
    #endif
  }
}

//---------------------------  USART2 ISR HANDLER  -----------------------------------
extern "C" void __irq_usart2(void){
  if((USART2_BASE->CR1 & USART_CR1_IDLEIE) && (USART2_BASE->SR & USART_SR_IDLE)){
    USART2_BASE->CR1 &= ~USART_CR1_IDLEIE;                           //Temporarily disable the Idle interrupt
    gps_data_available = true;
    gps_data_len = GPS_PACKET_SIZE - DMA1_BASE->CNDTR6;                //Bytes received = Max bytes - bytes remaining
    #ifdef DEBUG
    usart_count_irq++;
    #endif

    temp_var =  USART2_BASE->SR; //reset flags by reading the SR and DR registers
    temp_var =  USART2_BASE->DR;
    dma_disable(DMA1, DMA_CH6);             //disable the re-enable DMA channel to reset for the next burst of data
    dma_enable(DMA1, DMA_CH6);
    USART2_BASE->CR1 |= USART_CR1_IDLEIE;                           //Re-enable the interrupt
  }

  //If receive or transmit data for conventional serial communication (not DMA)
  if(((USART2_BASE->CR1 & USART_CR1_RXNEIE) && (USART2_BASE->SR & USART_SR_RXNE)) || ((USART2_BASE->CR1 & USART_CR1_TXEIE) && (USART2_BASE->SR & USART_SR_TXE))){ 
    usart_irq(USART2->rb, USART2->wb, USART2_BASE);                 //This routine is in file usart_private.h
  }
}

//-------------------------------------------------------------------------------------------------------------------------------------
void find_gps(void){
  Serial1.begin(9600);
  delay(250);

  #ifdef DEBUG
  Serial.print("Checking for GPS...Please wait...");
  #endif

  //flush the RX buffer  
  char temp_char;
  while (Serial1.available()){
    temp_char = Serial1.read();
  }
  delay(1100);            // delay to allow time for GPS to transmit new data (default 1 burst/sec @ 9600 baud)

  if(Serial1.available()){
    gps_exists = true;
    #ifdef DEBUG
    Serial.println("GPS FOUND");
    #endif
  }  
  else{
    gps_exists = false;
    #ifdef DEBUG
    Serial.println("GPS NOT found");
    #endif
  }
}

//------------------------------------------------------------------------------------------------------------------
void gps_setup(void) {

  Serial1.begin(9600);
  delay(1000);

  //Disable GPGSV messages by using the ublox protocol.
  uint8_t Disable_GPGSV[11] = {0xB5, 0x62, 0x06, 0x01, 0x03, 0x00, 0xF0, 0x03, 0x00, 0xFD, 0x15};
  Serial1.write(Disable_GPGSV, 11);
  delay(1000);   //A small delay is added to give the GPS some time to respond @ 9600bps.

  //                           sync1 sync2 class ID    Len   Len   Port  rsvd, TX rdy pin, UART mode-------------,
  uint8_t Set_to_57kbps[28] = {0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xD0, 0x08, 0x00, 0x00,
                               0x00, 0xE1, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE2, 0xE1};
                              // baud rate-uint------,inProtoMask,OutProtoMsk, flags-----,reserved---, checksums

   //                           sync1 sync2 class ID    Len   Len   Port  rsvd, TX rdy pin, UART mode-------------,
  uint8_t Set_to_115kbps[28] = {0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xD0, 0x08, 0x00, 0x00,
                               0x00, 0xC2, 0x01, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC4, 0x96};
                              // baud rate-uint------,inProtoMask,OutProtoMsk, flags-----,reserved---, checksums

  Serial1.write(Set_to_115kbps, 28);
  delay(350);

  Serial1.begin(115200);
  delay(350);

  //Set the refresh rate to desired Hz by using the ublox protocol.
  //                        sync1 sync2 class ID    Len   Len   MeasRate ms,navRate cyc,timeRef---,
  uint8_t Set_to_5Hz[14] = {0xB5, 0x62, 0x06, 0x08, 0x06, 0x00, 0xC8, 0x00, 0x01, 0x00, 0x01, 0x00, 0xDE, 0x6A};
  //                         sync1 sync2 class ID    Len   Len   MeasRate ms,navRate cyc,timeRef---,
  uint8_t Set_to_10Hz[14] = {0xB5, 0x62, 0x06, 0x08, 0x06, 0x00, 0x64, 0x00, 0x01, 0x00, 0x01, 0x00, 0x7A, 0x12}; 
  //                         sync1 sync2 class ID    Len   Len   MeasRate ms,navRate cyc,timeRef---,
  uint8_t Set_to_19Hz[14] = {0xB5, 0x62, 0x06, 0x08, 0x06, 0x00, 0x34, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4A, 0xF2};  //This is the fastest it will go
  Serial1.write(Set_to_19Hz, 14);
  delay(350);   //A small delay is added to give the GPS some time to respond.

//===========================  checksum  calculation if needed  =====================================
/*  
  uint8_t ChkA, ChkB, i;
  ChkA=0;
  ChkB=0;
  for (i=2; i<12; i++){
    ChkA += Set_to_19Hz[i];
    ChkB += ChkA;
  }

  Serial.print(ChkA,HEX);
  Serial.print(", ");
  Serial.println(ChkB, HEX);

*/
}

//------------------------------------------------------  GPS DMA SETUP  --------------------------------------------------------------
void gps_dma_setup(void){

  //Configure the USART per section 27.3.3 of the Reference Manual

  Serial1.end();
  delay(100);

  rcc_clk_enable(USART2->clk_id);                               //Activate the clock for the usart peripheral
  USART2_BASE->CR1 = 0;                                         //Reset control register to initialize.
  USART2_BASE->CR2 = 0;                                         //Reset control register to initialize.
  USART2_BASE->CR3 = 0;                                         //Reset control register to initialize.
  USART2_BASE->CR1 = USART_CR1_UE;                              //Enable USART.
  USART2_BASE->CR3 |= USART_CR3_DMAR;// | USART_CR3_EIE;        //Enable DMA mode on USART receiver. Error interrupt enable
  usart_set_baud_rate(USART2, USART_USE_PCLK, 115200);          //Set baud rate using LibMaple function
  USART2_BASE->CR1 |= USART_CR1_RE;                             //Enable receive mode only
  temp_var=  USART2_BASE->SR;                                   //Read the Status Register followed by Data register to reset latched status flags
  temp_var=  USART2_BASE->DR;                                   //This should be done if an error interrupt occurs. EIE

  dma_init(DMA1);                                               //Enable RCC clock for DMA1
  //Priority, Memory Address Increment, Transfer Error Interrupt, Half-xfer Interrupt, Xfer Complete Interrupt, Circular Transfer mode
  dma_mode = DMA_PRIORITY_LOW | DMA_CCR_MINC | DMA_CCR_TEIE | DMA_CCR_HTIE | DMA_CCR_TCIE | DMA_CCR_CIRC;
  dma_setup_transfer(DMA1, DMA_CH6, &USART2_BASE->DR, DMA_SIZE_8BITS, &gps_read_buffer, DMA_SIZE_8BITS, dma_mode);  //Default Mem-to-Mem=0, Dir=0 (read from peripheral)
  dma_set_num_transfers(DMA1, DMA_CH6, GPS_PACKET_SIZE);            //Set the number of bytes to be received
  dma_enable(DMA1, DMA_CH6);                                        //Enable DMA transfer for USART2.  Data now going into buffer in circular mode
  //nvic_irq_enable(NVIC_DMA_CH6);                                  //If Desired: Enable DMA_CH6 interrupts to fire. May be enabled by dma_attach_interrupt.
  //dma_attach_interrupt(DMA1, DMA_CH6, dma1_channel6_isr);         //If Desired: Declare and enable the DMA interrupt for channel 6 (for USART2 RX events)

  //nvic_irq_enable(NVIC_USART2);                                   //Enable if I want the USART2 interrupts to fire.
  nvic_irq_disable(NVIC_USART2);                                   //??? Test - Remove when done

  //while((USART2_BASE->SR & USART_SR_IDLE) == 0);                             //wait for the receive line to go idle before enabling DMA
  //USART2_BASE->CR1 |= USART_CR1_IDLEIE;                           //IDLE Interrupt Enable if you want to use this

  #ifdef DEBUG  
  Serial.print("USART2->CR1 = ");                                 //??? for debug
  Serial.println((uint32_t)USART2_BASE->CR1, BIN);
  #endif

  temp_var=  USART2_BASE->SR;                                       //Read the Status Register followed by Data register to reset NE (Noise Error)
  temp_var=  USART2_BASE->DR;                                       //This should be done if an error interrupt occurs. EIE

  nvic_globalirq_enable();                                          //Required for any interrupts to function                                           
  nvic_irq_enable(NVIC_USART2);                                     //Enable interrupts from USART2.  An interrupt calls __irq_usart2() function

  if(USART2_BASE->SR & USART_SR_IDLE){                                //Just prior to enabling interrupt, reset flags in status register
    temp_var=  USART2_BASE->SR;                                       //Read the Status Register followed by Data register to reset NE (Noise Error)
    temp_var=  USART2_BASE->DR;                                       //This should be done if an error interrupt occurs. EIE
  }
  USART2_BASE->CR1 |= USART_CR1_IDLEIE;                             //Enable the USART Line IDEL interrupt.  
}