循环缓冲区突发时缺失的最后一个字节

huangapple go评论48阅读模式
英文:

Missing Last Byte On Circular Buffer Burst

问题

感谢另一位用户的帮助,我已经实现了一个简单的循环缓冲区来读取一些UART数据,进行一些小的处理,然后在另一个UART上进行传输。根据与外部设备的一些用户交互,我接收到一串10-20个6字节的数据包。我有一个已知的SOF,它是两个值中的一个,然后是四个数据字节,然后是一个EOF字节。前XX个数据包总是具有第一个SOF值,而最后一个数据包总是具有另一个已知的SOF值。问题是,当我得到第二个SOF值时,IRQ不再触发。因此,在我的状态机中,我错过了EOF字节,循环缓冲区指针也没有增加,因此这个最后一个数据包没有被传输。正确的最后一个数据包已加载到循环缓冲区中(我不存储EOF字节),但由于我的头和尾索引相等(因为我没有在最后一个字节上继续到STATE_WAIT_EOF状态,这将增加头缓冲区的索引),头索引没有增加,数据包没有被处理/传输。

这是IRQ处理程序中的状态机:

void USART2_IRQHandler(void)
{
    uint8_t Curr_Byte = *RX_Touch_Data;

    switch(Pkt_State)
    {
        case STATE_WAIT_SOF:
        {
            Data_Indx = 0;

            if(( Curr_Byte == TD_PKT) || ( Curr_Byte == LO_PKT ))
            {
                Msg_Buff[Buff_Head_Indx].RX_Data[Data_Indx] = Curr_Byte;                
                ++Data_Indx;
                Pkt_State = STATE_RX_DATA;
            }
         }
         break;

        case STATE_RX_DATA:
        {
            Msg_Buff[Buff_Head_Indx].RX_Data[Data_Indx] = Curr_Byte;
            ++Data_Indx;

            if( Data_Indx == DATA_SIZE )
            {
                Pkt_State = STATE_WAIT_EOF;

                if (Msg_Buff[Buff_Head_Indx].RX_Data[0] == LO_PKT)
                {
                    LO_Pkt = TRUE;  //Set flag indicating got full data buff of last pkt
                }
            }
        }
        break;

        case STATE_WAIT_EOF:
        {
            if(LO_Pkt == TRUE)
            {
                DataFull = TRUE;    //Set breakpoint here, never hits
            }
        
            if( Curr_Byte == EOF_PKT )
            {
                uint8_t New_Buff_Head_Indx = ( Buff_Head_Indx + 1 ) % NUM_MESSAGES;        

                if( New_Buff_Head_Indx != Buff_Tail_Indx)
                {
                    Buff_Head_Indx = New_Buff_Head_Indx;
                }
                else
                {
                    //No space; msg will be lost
                }
            }

            Pkt_State = STATE_WAIT_SOF;
        }
        break;

        default:
        break;
    }

    HAL_UART_IRQHandler(&huart2);                       //Let HAL clean up flags

    HAL_UART_Receive_IT(&huart2, RX_Touch_Data, 1);    //Get next byte
}

当然,我还在进入主循环之前调用了HAL_UART_Receive_IT函数一次,以启动这个过程。

关于最后一个数据包,设备发送的数据包没有什么“特别”的地方。我 怀疑 如果我可以强制设备只发送一种类型的数据包,我会发现相同的情况:最后一个数据包仍然不会被发送(不幸的是,我不能这样做)。此外,请注意,如果通过新的用户交互触发新的数据包传输,第一个数据包的最后一个数据包会被“强制”发送出去(这是有道理的,因为头索引会递增)。因此,我可以与十个用户交互进行十次数据包传输,一切都很正常,只是最后一个数据包丢失了。

是否有人看到我漏掉了什么?希望我的解释有意义。

编辑:仅供测试,我扩大了循环缓冲区的大小,使其是我从十个数据包传输中期望的大小的10倍。没有变化。

我已临时实施了一个“补丁”,在最后一个数据包上不寻找EOF字节。这种方法有效,但充其量只是一个修复方法,我想知道为什么没有这个问题时它不起作用。

编辑2:

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, 
uint8_t *pData, uint16_t Size)
{
   /* Check that a Rx process is not already ongoing */
   if (huart->RxState == HAL_UART_STATE_READY)
   {
       if ((pData == NULL) || (Size == 0U))
   {
       return HAL_ERROR;
   }

   /* Set Reception type to Standard reception */
   huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

   if (!(IS_LPUART_INSTANCE(huart->Instance)))
  {
     /* Check that USART RTOEN bit is set */
     if (READ_BIT(huart->Instance->CR2, USART_CR2_RTOEN) != 0U)
     {
        /* Enable the UART Receiver Timeout Interrupt */
        ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_RTOIE);
     }
   }

   return (UART_Start_Receive_IT(huart, pData, Size));
 }
 else
 {
    return HAL_BUSY;
 }
}

我在HAL_BUSY和HAL_ERROR条件下设置了标志,但它们从未触发。

唯一其他可能相关的代码是检查可用数据包的尾索引的地方:

if( Buff_Head_Indx != Buff_Tail_Indx )                                     
//There are available packets to send
{
    Result = Process_Touch_Data();
    Buff_Tail_Indx = ( Buff_Tail_Indx + 1) % NUM_MESSAGES;                
}
英文:

Thanks to help from another user, I have implemented a simple circular buffer to read in some UART data, do some minor processing, then transmit on another UART. I receive a stream of 10-20 6-byte packets based on some user interaction with an outside device. I have a known SOF that is one of two values, four data bytes, then a EOF byte. The first XX packets always have the first SOF value, and the last packet always has the other known SOF value. The issue is that when I get that second SOF value, the IRQ doesn't hit again. As such, I miss the EOF byte in my state machine and the circular buffer pointer doesn't increment, so this last packet doesn't get transmitted. The correct final packet is loaded in the circular buffer (I don't store the EOF bytes), but because my head and tail indices are equal (because I don't proceed to the case STATE_WAIT_EOF state on the final byte, which would increment the head buffer), the head index doesn't get incremented, and the packet doesn't get processed/transmitted.

Here is the state machine in the IRQ handler:

void USART2_IRQHandler(void)
{
    uint8_t Curr_Byte = *RX_Touch_Data;

    switch(Pkt_State)
    {
	    case STATE_WAIT_SOF:
	    {
		    Data_Indx = 0;

		    if(( Curr_Byte == TD_PKT) || ( Curr_Byte == LO_PKT ))
		    {
			    Msg_Buff[Buff_Head_Indx].RX_Data[Data_Indx] = Curr_Byte;				
			    ++Data_Indx;
			    Pkt_State = STATE_RX_DATA;
		    }
	     }
	     break;

	    case STATE_RX_DATA:
	    {
		    Msg_Buff[Buff_Head_Indx].RX_Data[Data_Indx] = Curr_Byte;
		    ++Data_Indx;

		    if( Data_Indx == DATA_SIZE )
		    {
			    Pkt_State = STATE_WAIT_EOF;

			    if (Msg_Buff[Buff_Head_Indx].RX_Data[0] == LO_PKT)
			    {
				    LO_Pkt = TRUE;  //Set flag indicating got full data buff of last pkt
			    }
		    }
	    }
	    break;

	    case STATE_WAIT_EOF:
	    {
		    if(LO_Pkt == TRUE)
		    {
			    DataFull = TRUE;    //Set breakpoint here, never hits
		    }
		
            if( Curr_Byte == EOF_PKT )
		    {
			    uint8_t New_Buff_Head_Indx = ( Buff_Head_Indx + 1 ) % NUM_MESSAGES;		

			    if( New_Buff_Head_Indx != Buff_Tail_Indx)
			    {
				    Buff_Head_Indx = New_Buff_Head_Indx;
			    }
			    else
			    {
				    //No space; msg will be lost
			    }
		    }

		    Pkt_State = STATE_WAIT_SOF;
	    }
	    break;

	    default:
	    break;

    }

    HAL_UART_IRQHandler(&huart2);                       //Let HAL clean up flags

    HAL_UART_Receive_IT( &huart2, RX_Touch_Data, 1 );	//Get next byte
}

Of course, I also call HAL_UART_Receive_IT function once before entering my main while loop to kick off the process.

There isn't anything "special" about the last packet the device sends. I suspect if I could force the device to ONLY send the one type of packet, I'd find the same situtation: the final packet still wouldn't get sent (unfortunately, I can't do that). Also, note that if I trigger a new burst of packets via a new user interaction, the last packet of the FIRST burst gets "forced" out (which makes sense, as the head index gets incremented). So I could have ten user interactions with ten bursts of packets, and all will be fine, except the last one, which would be missing the final packet.

Does anyone see what I'm missing? I hope my explanation makes sense.

EDIT: Just for test, I blew out my circular buffer size to 10x what I expect from say, ten bursts. Just wnated to eliminate the possibility of any wraparound index issues. No change.

I've temporarily implemented a "patch" where I DON'T look for the EOF byte on the last packet. This works, but is a hack at best and I'd like to understand why it's not working without this.

EDIT2:

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, 
uint8_t *pData, uint16_t Size)
{
   /* Check that a Rx process is not already ongoing */
   if (huart->RxState == HAL_UART_STATE_READY)
   {
       if ((pData == NULL) || (Size == 0U))
   {
       return HAL_ERROR;
   }

   /* Set Reception type to Standard reception */
   huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

   if (!(IS_LPUART_INSTANCE(huart->Instance)))
  {
     /* Check that USART RTOEN bit is set */
     if (READ_BIT(huart->Instance->CR2, USART_CR2_RTOEN) != 0U)
     {
        /* Enable the UART Receiver Timeout Interrupt */
        ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_RTOIE);
     }
   }

   return (UART_Start_Receive_IT(huart, pData, Size));
 }
 else
 {
    return HAL_BUSY;
 }
}

I've set flags on the HAL_BUSY and HAL_ERROR conditions, they are never hit.

The only other potentially relevent code is where the tail index is examined for available packets:

if( Buff_Head_Indx != Buff_Tail_Indx ) 									 
//There are available packets to send
{
    Result = Process_Touch_Data();
	Buff_Tail_Indx = ( Buff_Tail_Indx + 1) % NUM_MESSAGES;				
}

答案1

得分: 3

问题

您的数据处理全部延迟了一个字节。以下是事件的顺序:

您调用 HAL_UART_Receive_IT( &huart2, RX_Touch_Data, 1 ); 这会启用UART上的接收中断。

UART上收到一个字节(假设是 SOF)。然后调用 USART2_IRQHandler()。您从 *RX_Touch_Data 读取一个字节,该字节目前没有设置为有意义的内容,并对其进行处理。

接着您调用 HAL_UART_IRQHandler()。这会从UART中读取 SOF 字节并将其存储在 *RX_Touch_Data 中。

当接收到下一个字节(假设是 0x53)时,您的状态机处理 SOF,然后HAL将 0x53 存储在 *RX_Touch_Data 中。无限循环。您始终落后一个字节。

在最后一个 EOF 进来后,它会被缓冲在 *RX_Touch_Data 中,并且由于没有更多的字节出现,状态机不会处理它。

建议的解决方案

在您的中断处理程序中自己从UART2读取字节,类似于以下方式:

uint8_t Curr_Byte = UART2->DR & 0xff;

并且不要调用 HAL_UART_IRQHandler()

我还建议您自己启用接收中断。然后,您不需要再调用 HAL_UART_Receive_IT()。每次接收到一个字节时,您将获得一个中断。可能这就足够了:

/* 启用UART数据寄存器非空中断 */
__HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE);
英文:

Problem

Your data handling is all delayed by one byte. Here is the sequence of events:

You call HAL_UART_Receive_IT( &huart2, RX_Touch_Data, 1 ); This enables the receive interrupt on the UART.

A byte (let's say SOF) is received on the UART. USART2_IRQHandler() gets called. You read a byte from *RX_Touch_Data, which is not currently set to anything meaningful, and process that.

You then call HAL_UART_IRQHandler() This reads the SOF byte from the UART and stores it in *RX_Touch_Data.

When the next byte is received (let's say 0x53), your state machine processes the SOF, and then the HAL stores the 0x53 in *RX_Touch_Data. Ad-inifinitum. You're always one byte behind.

After the final EOF comes in, it is buffered in *RX_Touch_Data, and since no more bytes appear, it is not processed by the state machine.

Suggested solution

Read the byte from UART2 yourself in your interrupt handler, something like:

uint8_t Curr_Byte = UART2->DR & 0xff;

and do not call HAL_UART_IRQHandler().

I'd also recommend just enabling the receive interrupt yourself. Then you don't need to call HAL_UART_Receive_IT() any more. You just get an interrupt every time a byte is received. This might be enough:

/* Enable the UART Data Register not empty Interrupt */
__HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE);

答案2

得分: 0

IMHO,您的状态集合定义不清晰,因此无法实现协议的自动机。

在您的自动机中,只定义了正确的输入情况,并且通常情况下,如果输入不是您接受的输入,就会避免切换到不同的状态。这会导致您的接收器陷入一种状态,其中您可以接收到格式不正确的数据包、线路噪音或简单的错误输入,并且无法从该点正确地恢复。

接受输入的协议自动机必须考虑所有输入状态以及每个可能输入的转换,即使您认为它永远不会发生。当您如此系统地分析输入时,您会覆盖所有情况,您的自动机可以正确地从语法错误中恢复。

我建议将自动机定义为一个表格,每个状态一行,每个可能输入一列。在表格的每个单元格中,我会放置该输入的下一个状态,以及在我识别到某些内容时采取的任何操作。这可以通过指向执行工作的例程的指针来实现。

考虑所有组合...在您发布的代码中,您只考虑了数据按照某种(正确的)顺序到达的情况,但这可能并不总是如此。如果您检查您的自动机,有可能您会接收到一个 TD_PKTLO_PKT,然后切换到 STATE_RX_DATA 状态。如果之后发生了一些错误,比如您拔掉连接器然后重新插上,您会在识别到新的不正确数据包之前丢失一个完整的数据包(因为您将拔掉之前的数据包内容与下一个数据包中的数据相加,这应该是不正确的),因此必须始终存在一些帧错误状态,从中您可以在任何输入(除了新帧的开始)时转到空闲状态(在新帧的开始情况下,您会再次进入一个新的数据包内部)。

在我看来,您应该根据到目前为止已识别的内容来命名您的状态,而不是根据期望的内容。您应该能够在任何状态下开始(并适当地恢复):

  • Idle 状态。起始状态。您尚未接收到任何有效数据。在此状态下:
    • TD_PKT_SOF 切换到 IN_DATA_TD_PKT 状态。这意味着您已接收到有效的标头字符,并准备开始接收缓冲区数据。您应该清空缓冲区(如下所述,可以选择将起始字符添加到缓冲区中),因为您将开始添加数据字节。
    • LO_PKT_SOF 切换到 IN_DATA_LO_PKT 状态。这表示识别出了错误(部分接收到的数据包没有数据包结束分隔符)。您可以向用户报告错误(可能包括部分接收到的数据包)。您应该清空缓冲区(如上所述,可以选择将其添加到缓冲区中),然后开始添加新的数据字节。
    • END_PKT 在此状态下忽略此字符。您尚未启动任何数据包,因此接收此字节表示线上的杂散字节。
    • 其他任何内容,保持在 Idle 状态。您在此状态下将此输入视为与 END_PKT 完全相同。之所以将其描述为分开的原因是为了保持表格格式。
  • IN_DATA_TD_PKT 状态。在此状态下,如果收到:
    • TD_PKT_SOF 您已收到新数据包的开始。这应该被视为错误。您应该向用户发送错误信息,可能包括传递了格式不正确的缓冲区内容。您还必须清空缓冲区,因为您还要开始收集新数据包的字节内容。然后切换到 IN_DATA_TD_PKT(这意味着包类型现在与之前相同,没有状态更改)。
    • LO_PKT_SOF 与上面相同,但您开始了不同的数据包类型,这种情况下,您可以像上面那样向用户报告错误,您应该清空缓冲区,但现在必须切换到不同的状态 IN_DATA_LO_PKT,因为现在数据包类型不同了。
    • END_PKT 返回一个 TD 数据包类型的缓冲区给用户。切换到 Idle 状态,可以选择清空缓冲区内容。在这里清空缓冲区是多余的,因为您在开始新数据包时总是清空它。
    • 任何其他内容,将其添加到缓冲区,不切换状态。
  • IN_DATA_LO_PKT 状态。如果收到:
    • TD_PKT_SOF 您应该宣告错误(这可以根据缓冲区是否部分填充而进行,因此您不会因为拔掉电缆而获得虚假错误)。您需要切换到状态 IN_DATA_TD_PKT,因为数据包类型已更改。
    • LO_PKT_SOF 您应该保持在此状态,但在缓冲区非空时应该报告错误(与 IN_DATA_TD_PKT 状态中所做的方式相同,但对称地)。
    • END_PKT 返回一个 LO 数据包类型的缓冲区给用户。切换到 Idle 状态,并且可以选择清空缓冲区。在这里清空缓冲区是不必要的,因为您始终会在开始新数据包时清空它。

正如您在此描述中所看到的,所有状态和输入数据都已考虑。

英文:

IMHO you have a poorly defined set of states and so, a bad automaton to implement the protocol.

You have defined in your automaton only the correct input case, and as a general rule, you avoid to move to a different state in case the input is not the one you accept. This leads you to a state in which you can receive a misformed packet, line noise of simply bad input, and lock your receiver from that point on.

A protocol automaton to accept input must consider all input states, and the transitions for every possible input, even in the case that you think it will never happen. When you analise your input so systematically, you cover all the cases, and your automaton recovers correctly from syntax errors.

I should define the automaton as a table, one row for each state, one column for each possible input. I would put on each cell of the table, the next state on that input, and also any action in case I have recognised something, This can be implemented with a pointer to a routine that does the work.

Consider all combinations... in the code you post, you only consider that the data comes in some (correct) sequence, but this can be not the case. If you check your automaton, there's a possibility that you receive a TD_PKT, LO_PKT and switch to state STATE_RX_DATA. If then it happens some error, like you have umplugged the connector and plugged it again, you will lose a full packet before you recognice a new, incorrect packet (because you will have the packet contents up to the unplug, added with the data comming from the next packet, and that should be incorrect) there must be always some framing error state from which, on any input (except the start of a new frame) you go to the idle state (and in case of the start of a new frame, you get inside a new packet again)

In my opinion, you should name your states based on what has been recognized upto the considered state, and not on what is being expected, You should be capable to start (and recover appropiately) on whatever state you may be:

  • Idle state. Starting state. You have not received anything valid yet. In this state:
    • TD_PKT_SOF switches you to IN_DATA_TD_PKT state. It means you have received a valid header char and you prepare to start receiving buffer data. You should empty the buffer (and see below, add the start character to the buffer, optionally), as you will start adding data bytes.
    • LO_PKT_SOF switches you to IN_DATA_LO_PKT state. This recognizes an error (a partially received packet with no end of packet delimiter) You can signal the error to the user (possibly including the partially received packet) You should empty the buffer (and as described above, add it to the buffer optionally), and you will start adding new data bytes.
    • END_PKT ignore this character in this state. You have not started any packet, so receiving this byte represents a spurious byte on the line.
    • anything else, remain in Idle state. You treat this input exactly as END_PKT in this state. It is described separate to maintain the table format.
  • IN_DATA_TD_PKT state. In this state, if you receive:
    • TD_PKT_SOF you have received the start of a new packet. This should be considered an error. You should transmit to the user an error, possibly passing up the malformed buffer contents. You must also empty the buffer, because you are also starting the collection of a new packet, as the bytes coming next should start filling a new buffer contents. You will switch to IN_DATA_TD_PKT (which means no state change as the packet type now is the same as before).
    • LO_PKT_SOF same as above, but you start a different packet type, in this case you can signal an error to the user as above, you should empty the buffer, but you must switch now to a different state IN_DATA_LO_PKT, as now the packet type is different.
    • END_PKT return a buffer to the user of a TD packet type. Switch to Idle state, and optionally you can empty the buffer contents. Emptying the buffer is done on start of a new packet, so it's redundant to be done here also.
    • any other thing, add it to the buffer and don't switch state.
  • IN_DATA_LO_PKT. If you receive:
    • TD_PKT_SOF you should declare an error (this can be done depending if the buffer is partially filled or not, so you will not get spurious errors due to cable umplugging) You need to switch to state to IN_DATA_TD_PKT, as the packet type has changed.
    • LO_PKT_SOF you should remain in this state, but you should signal an error on a non empty buffer (as done in state IN_DATA_TD_PKT state, but symmetrically)
    • END_PKT return a buffer to the user of a LO packet type. Switch to Idle state, and optionally empty the buffer. Emptying the buffer here is not necessary, as you always empty it when starting a new packet.
    • any other thing, add it to the buffer.

As you see in this description, all states and input data are considered. You can implement this as a nested double switch (in any order) or as a table controlling a loop in which the interrupt routine is called for each received character, the character is mapped to its character class and then processed by finding the table entry corresponding to state and input character class, then the next state is computed and assigned, and the actions pointer is checked and, if not NULL, executed to perform the actions corresponding to it. The table approach results, in general in more compact code, and allows you to share the same code to several cases very easily. If I had to implement this, I'd write an input mapping table, which maps all the bytes (256 entries) into character classes, which are the ones considered as input. I'd use a compact 3x4 table (three states, four input classes) and based on that, start testing the code. In case on errors you need some way to recover your automaton state, more if you are implementing this as some kernel driver. The state also allows you to distinguish what kind of packet you have received. IMHO, if you designed two different end of packet bytes instead of two different start of packet, you would not need to maintain double states and you will get a more compact automaton.... but that's a requirement of the problem and normally not a place in where you are free to change design.

Another thing is that, as you see, I have not included the control characters in the buffer, but you can do it if desired.... in that case, apart of emptying the buffer when you start gathering data, you have to add the first start of packet to the buffer (the one that corresponds, this is not necessary if you tag somehoe the packet type when you return it to the user) and you will need to add the packet end character to the buffer, once a full packet is recognized.

huangapple
  • 本文由 发表于 2023年5月17日 22:29:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76273228.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定