Receive Data using IDLE LINE
This is the 5th tutorial in the series on the UART peripheral of STM32 Microcontrollers. In this series we will cover different ways of transmitting and receiving data over the UART protocol. We will also see different UART modes available in the STM32 microcontrollers and how to use them.
In the previous tutorial we saw how to receive the data over the UART using the DMA. We used both Normal and Circular mode to receive data. Although we were able to receive the data of large size very efficiently, but we needed to know the size of the data in advance. This was necessary to process the data.
In today’s tutorial, we will utilize another feature of the STM32 UART, i.e. the IDLE Line. We will use this feature to receive the data, large or small, of unknown size. The advantage we have over the previous methods is, we do not need to know the size of the data in advance, instead the function’s callback will inform us how much size is received.
What is IDLE Line?
We receive the data via the UART at a fixed baud rate. Because of this there is always a small delay between two consecutive bits/Bytes and this delay remains more or less the same. When the sender stops sending data for a while, the UART peripheral detects it as an IDLE Line. We will use this IDLE line to identify that the sender has stooped sending the data and we can process the received data.
The sender stops sending data in 2 cases. One is when it has sent the entire data and stops. And the second scenario is mentioned below.
- Let’s say we are receiving some large data using the UART.
- This data is sent in chunks.
- There is some small delay between two chunks, and for this delay the line remains IDLE.
- That’s it, whenever the MCU detects this idle line, an interrupt will be triggered, and we can process the data, and prepare for the next chunk.
We will cover both cases in today’s tutorial.
IDLE Line using Interrupt
The interrupt method should be used when receiving small amount of unknown data. Here the interrupt is triggered when all the required data has been received or an IDLE Line is detected before that. We should note that the receive buffer should be large enough to store all the data.
We shall enable the UART interrupt in the cube MX as shown below.
Below is the code to receive data using the interrupt mode.
uint8_t RxData[30];
int indx = 0;
int main()
{
....
HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 30);
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(1000);
}
}
The RxData buffer is defined to store a maximum of 30 bytes of data. The indx variable will be used to store the size. In the main function we will call the function HAL_UARTEx_ReceiveToIdle_IT to receive 30 bytes of data in the interrupt mode.
When 30 bytes of data is received or an IDLE Line is detected before that, an interrupt will trigger and the RX event callback is called. We can process the received data in the callback.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
indx = Size;
HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 30);
}
The Size parameter of this callback represents how many data bytes has been received when the interrupt is triggered. We will store the size to the indx variable so that it can be later used in the while loop or any other function.
The interrupt is disabled after it gets triggered, so we need to call the interrupt again at the end of the callback. This is to make sure that the data reception is continuous.
Below are the images showing the result of the above code.
The first image shows the 9 data bytes sent by the serial monitor and the second image shows the data received in the RxData buffer along with the value of the indx variable.
We can use the interrupt to receive small data of unknown size. As long as the data size is less than or equal to the buffer size, it should work fine.
IDLE Line using DMA
To receive large data, we will use the DMA. Below is the cubeMX configuration for the DMA.
Note that the DMA is enabled in the Circular mode. This is necessary if we are dealing with the large data.
If the data is in few kilobytes, we can receive the entire data in the buffer. This can be done by calling the receive function just once. Below is the code for the same.
uint8_t RxData[4096];
uint16_t indx = 0;
int count = 0;
int main()
{
....
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, RxData, 4096);
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(1000);
}
}
The RxData buffer is defined to store a maximum of 4096 bytes of data. The indx variable will be used to store the size. The count variable tracks how many times the callback was called. In the main function we will call the function HAL_UARTEx_ReceiveToIdle_DMA to receive 4096 bytes of data in the DMA mode.
When 4096 bytes of data is received or an IDLE Line is detected before that, an interrupt will trigger and the RX event callback is called. We can process the received data in the callback.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
indx = Size;
count++;
}
The Size parameter of this callback represents how many data bytes has been received when the interrupt is triggered. We will store the size to the indx variable so that it can be later used in the while loop or any other function. We will also increment the count variable to track how many times this callback was called.
Below are the images showing the output of the above code.
You can see above the entire data is received by the MCU. The value of the indx variable is equal to the size of the data file we sent.
When we send a large amount of data, the software sometimes send the data in smaller chunks (23 in the above picture). This makes the interrupt to trigger many times during the transmission. In circular mode, the new data is stored at the very next position in the buffer. Also the Size variable preserves its value between different calls and the value keeps adding to the previous value. This is why we see the value of the indx variable equal to the actual data transferred and not equal to the number of data bytes received during the last call.
The count variable shows how many times this callback was called during the transfer.
The above method works fine when we have the data in few kilobytes as we can store the data in a buffer in the RAM. The data then can be processed in the callback itself or later in the while loop. The indx variable can be used as the size of the received data.
When we are receiving a very huge amount of data, mostly in megabytes, the above method can’t work. This is because we can not define a buffer large enough to store entire data in the RAM. In this case, we will use small buffer to receive the data, which will be then transferred to the file/Buffer in the SD card or external flash memory.
Below is the code which demonstrate the method mentioned above.
#define RXSIZE 256
uint8_t RxData[RXSIZE];
uint8_t FinalBuf[4096];
uint16_t indx1 = 0, indx2=0, rxcplt=0;
int count = 0;
int main()
{
....
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, RxData, RXSIZE);
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(1000);
}
}
The RxData buffer is defined to store a maximum of 256 bytes of data, while the FinalBuf can be assumed as a file in the SD card/external flash. The indx1 variable will be used to keep track of the position inside the RxData variable while the indx2 will be used to keep track of the position in the FinalBuf. The rxcplt variable will keep track of how many times the 256 bytes were received. This will help us update the indx1 and indx2 variables accordingly. The count variable tracks how many times the callback was called. In the main function we will call the function HAL_UARTEx_ReceiveToIdle_DMA to receive 256 bytes of data in the DMA mode.
When 256 bytes of data is received or an IDLE Line is detected before that, an interrupt will trigger and the RX event callback is called. We will process the received data in the callback.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
memcpy (FinalBuf+indx2, RxData+indx1, Size);
if (Size == RXSIZE)
{
rxcplt++;
indx2 = RXSIZE*rxcplt;
indx1=0;
}
else
{
indx2 = indx2 + (Size-indx1);
indx1 = Size;
}
}
In the callback we will transfer the data from RxData to the FinalBuf. The indx1 vaiable holds the current position in the RxData buffer where the copying will start from. Similarly the indx2 variable will hold the current position in the FinalBuf where the copying will be done to. The Size variable represents how many data bytes were received when the callback was called, and hence we will copy these number of bytes.
As I mentioned before, in circular mode the Size variable retains its value between different calls. If the received number of bytes are less than the RXSIZE (256), then we will simply update the values of indx2 and indx1 variables. The indx2 variable is updated, indx2 = indx2 + (Size-indx1), because the Size variable will keep updating its value. Therefore in order to find the actual number of bytes received in the current call, we need to subtract the previous value of the indx1 variable from the current size (Size-indx1).
If we have received 256 bytes, then the rxcplt will increment. Then we will update the indx2 variable as a multiple of 256 and reset the indx1 variable, so that new data can be copied from the beginning of the RxData buffer.
Below are the images showing the output of the above code.
As you can see above, the indx2 variable is the same as the size of the file sent by the software. The rxcplt is 8, which represents how many times the entire 256 bytes were received. The indx1 variable represents the number of bytes received after last time the 256 bytes were received.
256*8 + 128 = 2176.