omuzychko / StepperHub

3-axis UART Stepper Hub for 20$ (STM32 Nucleo 64 & Arduino CNC Shield)
73 stars 26 forks source link

Late response on uart #3

Open NKNguyenLX opened 5 years ago

NKNguyenLX commented 5 years ago

Hi Alex, thank you for your amazing works. I have managed to run your code successfully on my ST32F407GVT board. However, the UART response seem like to be delay for one commit. For instance, if i want to send the command " setX.minSPS" via UART, i have to send another command to get the first command running. Please let me know what i have to do, thank you.

omuzychko commented 5 years ago

I am using UART with DMA. Not to waste CPU cycles poling byte-by-byte from UART data register. CPU can be very loaded tracking the state of stepper motors in real-time when they are running too fast. So DMA is strongly required here. There are not much difficulties when sending data out asynchronously, just take the pointer to the data buffer and call UART_TransmitDMA(,,,).

But there is a big difficulty when receiving data over UART asynchronously with DMA:

When you call UART_ReceiveDMA(* buffer, int length) - you will get an interrupt request when full-buffer DMA transfer done. So you can process your data and change state of running motors. But this approach will only work if you have fixed length of your command buffer.

But according to my propitiatory text protocol - all commands have different length. So you can not set-up DMA RX routine for that. If command length is short than DMA RX buffer length - the DMA controller will wait until more bytes arrive to the buffer (until the buffer is full) before it will fire "RX-done" interrupt.

Alternatively - you may setup shorter DMA RX transfer (where buffer length smaller or equal to the shortest command you have in your communication protocol). So you will get your interrupt when the beginning of the command arrives over UART. But you have a problem when the command is longer than the buffer - you would have to restart UART DMA immediatelly to receive the renaming part of command. Also you should know somehow the length of the remaining part of the command (not supported by my protocol).

So what is the solution to this? The short answer - sniff into current DMA RX transfer state. And process data before the full transfer finished. Here is more detailed explanation:

So basically - we still offloading CPU a lot, because we are receiving incoming data in our memory without constantly monitoring the state of UART registers. But we are periodically checking in (by timer) if we got any data in our RX buffer to process a possible command. Timer handler is TIM8_TRG_COM_TIM14_IRQHandler(void) in main.c, it calls Serial_CheckRxTimeout();. Also this timer executes StpperController loop, to update steppers state. So it is kinda a controller-loop of all system. You may alternatively call these methods in main while(1){...} loop with HAL_Delay(..) in it. But having it in timer interrupt allows me to manage priorities between different events in the system. See the NVIC configuration - the number I've put there are very critical, otherwise you may miss the steps when motros are spinning.

Your problem is that you simply did not configure this timer or its interrupt properly. So you only see steppers to react when you send enough commands to fill out the full RX-buffer (so command controller is being fired not by the TIM interrupt, but by the DMA RX Transfer Finished interrupt ).

All this complexity is just for the sake of dealing with simple text protocol. If I used digital protocol - I would design it differently. Like: 1) first two bytes - command start header 2) second two bytes - command length 3) lenght bytes of actual data 4) 2 bytes of CRC sum

In this scenaro you know that every command starts with two bytes of some know header - so you setup DMA transfer for two bytes only. When its done - you check if its an known header, if not - you restart DMA again for next two bytes of possible header to arrive. If it was know header - you read setup DMA RX Transfer to read two bytes of lenght, and when length is knwon - you setup DMA RX Tranfer of known lenghts + bytes of CRC.

So as you see - machine-to-machine interfaces in binary format are way more efficient. That what I use in my personal projects, but this one is shared over GIT-hub for ease of use by general public. So human-readable text protocol is more preferred here, but causes a bit of complexity in implementation.

The opposite problem is with Transmitting I wanted to transmit asynchronously, kind of call UART_Transmit(...) without being worried that there there is another UART transmission is in progress. Otherwise - the Serial_WriteBytes(...) or Serial_WriteString(...) would be blocking calls. We can not start new UART DMA TX transfer, when the the previous one is in progress.

So the solution is similar - we have circular txBuffer which has UART DMA transfer restarted immediately when the previous transfer finished. It kinda implements a queue data structure. UART DMA pops data from the end of the queue whenever there is any data in it, from the other end of the queue (txBuffer array) we insert new data whenever any Serial_WriteXXX(...) method being called.

The txBuffer is fixed size. "Adding new data" to the queue means simply writing into this array some values at certain location defined by * txInPtr*. While the DMA TX transfer are controlled by another txOutPtr** pointer.

You may end up in condition when you write to much data into it, faster than UART is capable to send it out. In this case txInPtr will run through the full buffer and collide with txOutPtr. In this case everything in regard of UART stops - you will see SERIAL_TXOVERFLOWMSG in terminal. The txBuffer is then reset. So you loose fraction of incoming data on your terminal. (A classic "queue" data structure automatically expands its size in this case. creates twice bigger array and copies everything there from and old smaller array - but we are in embedded environment with restricted resources and can not afford that).

NKNguyenLX commented 5 years ago

Thank you again, i have figured it out. You just need to add any character except "0" to "9" at the end of your command and everything will run smoothly. To solve the problem about the inconsistent length of the command, i suggest defining an ending character of the command line such as "/n/r" or "end" to indicate the end of the command.