在 UART 通信中接收额外字节

Recieving extra byte in UART comms

我参与了一个项目,我需要从设备中提取一些数据并将其显示在 PC 上以供检查。我从中接收数据的设备发送一个字符串,其中包括设备 ID、当前模式、温度读数和电池读数。为方便起见,这些用逗号分隔。字符串的一个例子是:

01,03,66661242,28

所以这意味着设备 ID 为 1,模式为模式 3,温度读数为 36.6(这是 ASCII 小端格式),电池电量为 4.0V(以 ASCII 发送并除以 10 )

我无法控制发送数据的格式

我正在为此使用 STM32F091RC Nucleo 开发板,我的代码是:

#include "mbed.h"

Serial pc(PA_2, PA_3);
Serial Unit (PA_9, PA_10, 9600);                                                // 9600 baud rate - no parity - 1 stop bit

//Input pins
DigitalIn START(PB_8, PullUp);

void GetData();
void CheckData();

char Data[100];

int deviceId;
int Mode;
float TempReading;
float battReading;

unsigned char Ascii2Hex (unsigned char data)
{
    if (data > '9')data += 9;   // add offset if value > 9
    return (data &= 0x0F);
}

unsigned char Ascii2Char(unsigned char Offset)
{
    unsigned char Ans;
    Ans = Ascii2Hex(Data[Offset]);
    Ans = Ans<<4;
    Ans += Ascii2Hex(Data[Offset+1]);
    return(Ans);
}

float Ascii2Float(unsigned char Offset)
{
    float Bob;
    unsigned char Ans;
    Ans = Ascii2Hex(Data[Offset+6]);
    Ans = Ans<<4;
    Ans += Ascii2Hex(Data[Offset+7]);
    ((unsigned char*)&Bob)[3]= Ans;
    Ans = Ascii2Hex(Data[Offset+4]);
    Ans = Ans<<4;
    Ans += Ascii2Hex(Data[Offset+5]);
    ((unsigned char*)&Bob)[2]= Ans;
    Ans = Ascii2Hex(Data[Offset+2]);
    Ans = Ans<<4;
    Ans += Ascii2Hex(Data[Offset+3]);
    ((unsigned char*)&Bob)[1]= Ans;
    Ans = Ascii2Hex(Data[Offset]);
    Ans = Ans<<4;
    Ans += Ascii2Hex(Data[Offset+1]);
    ((unsigned char*)&Bob)[0]= Ans;
    return(Bob);
}

void DecodeString()
{
    char x;

    //numbers in brackets is where the data starts in the string
    deviceId = Ascii2Char(0);
    Mode = Ascii2Char(3);
    TempReading = Ascii2Float(6);
    x = Ascii2Char(15);
    battReading = (float)x/10;
    GetData();
}

void GetData()
{
    Unit.scanf("%s,",Data);  // scan the incoming data on the RX line
    pc.printf("%s,\n\r",Data);
    pc.printf("Device ID = %i\n\r", deviceId);
    pc.printf("Mode = %i\n\r", Mode);
    pc.printf("Temp = %.1f\n\r", TempReading);
    pc.printf("Bat = %.1f\n\n\r", battReading);
}

int main()
{
    while(1) {
        if(START == 0) {
            wait(0.1);
            DecodeString();
        }
    }
}

当我第一次开机按下按钮获取数据时,我收到的字符串前面多了一个0:001,03,66661242,28

这意味着数据不正确,因为数据已经移位,但是,如果我再次按下它,它会给出正确的字符串 但是打印的数据不正确 再按一次一切正常,并将继续工作,直到 Nucleo 板被重置。从我的串行监视器接收到的字符串和显示的数据的示例是:

001,03,33331342,28,
Device ID = 0
Mode = 0
Temp = 0.0
Bat = 0.0

01,03,CDCC1242,28,
Device ID = 0
Mode = 192
Temp = 0.0
Bat = 19.4

01,03,CDCC1242,28,
Device ID = 1
Mode = 3
Temp = 36.7
Bat = 4.0

不是编码专家,我是一个初学者。解码字符串的代码位是设计发送数据字符串的设备的工程师给我的。我需要帮助,但由于在家工作并且人们忙于其他事情,这不是一个紧迫的问题,所以帮助有限。

我尝试在不同的地方添加一些延迟(例如在原始 scanf 之后和打印之前),我也尝试了 scanf 函数 3 次作为实验,看看我是否可以绕过不正确的数据但是 none 其中有帮助。我尝试使用不同的 UART 引脚(STM32F091RC 64 引脚设备有 6 个可用)但我仍然得到相同的结果。我还将数据字节长度从 100 更改为 17,因为这是我期望收到的数量,但它仍然没有区别。

我已确保所有设备共享一个公共 GND 并仔细检查了所有硬件连接。

我只想第一次收到正确的数据并第一次显示正确的结果,但我似乎无法让它工作。

编辑

我现在尝试添加额外的几行。我正在使用 strlen 来计算字符串中的字节数。如果超过 17,我会重试。第一个问题已经解决了,但是第一组解码数据还是显示不正确:

String Length = 18

String Length = 17

01,03,66661242,28,
Device ID = 0
Mode = 192
Temp = 0.0
Bat = 19.4

String Length = 17

01,03,66661242,28,
Device ID = 1
Mode = 3
Temp = 36.6
Bat = 4.0

有什么方法可以确保数据第一次被正确解码,或者数据第一次被正确读取而不需要解决方法?

您似乎没有任何消息定界符来指示消息流的开始或结束。我认为这是因为您只处理接收 ASCII 数据。

一种选择是使用 strtok 将数据拆分为字符串(使用 ',' 作为分隔符)。

测试数组中是否返回了 4 个字符串。

然后对于第一个块只需使用 atoi 转换为整数。这样做“001”和“01”应该都转换为 1。

理想情况下,您应该在接收时检查邮件的格式,以防您没有收到完整的邮件,但据我目前所见,这并不是真正必要的。只需检查每个字符串的格式,例如如果它们包含非数字字符,则应丢弃直到并包括该点的数据。

编辑

我不明白 Temp 是如何编码的,但我有这个示例代码 此代码中的温度不正确:

#include <stdio.h>

#include <stdlib.h>
#include "string.h"

int main()
{
    char input[] = "001,03,66661242,28";

    char* pstr = strtok(input,",");
    int count =0;
    int ID =0;
    int Mode =0;
    double Temp =0.0;
    float Volt = 0.0;

    while(pstr!=NULL)
    {
        switch(count)
        {
            case 0:
                ID = atoi(pstr);
            break;
            case 1:
                Mode = atoi(pstr);
            break;

            case 2:
                Temp = strtod(pstr, NULL);
            break;

            case 3 :
                Volt = strtol(pstr, NULL ,16)/10;
            break;

        }
        printf("%s\n", pstr);
        pstr = strtok(NULL,",");
        count++;

    }

    if(count == 4)
    {
        printf("ID = %d\n", ID);
        printf("Mode = %d\n", Mode);
        printf("Temp = %.1f\n", Temp);
        printf("Voltage = %.1f\n", Volt);
    }
    else
    {
        printf("Error");

    }

}

这解决了您的问题:

您的代码编写方式,GetData() 正在打印 之前 中的“设备 ID”、“模式”、“温度”和“蝙蝠”数据获取的数据,而不是来自最近获取的数据。这就是为什么您的第一组数据全为零;在第一次通过时,所有这些变量仍然包含它们原始的未初始化值,因为它们是静态分配的“全局数据”,所以这些值全为零。 second time through, it's printing the results you obtained from the first reading,这给了你错误的“设备ID”值,因为字节流开头的额外零。最后,第三次,它打印了你从 second 阅读中获得的数据,这很好。

如果您只是重新排列部分代码,它会根据最近的样本打印数据。我没有尝试编译 & 运行 它,但在我看来这可能是对它的一个很好的重写,它将你的 DecodeString()GetData() 函数组合成一个函数:

void DecodeString()
{
    char x;

    // scan the incoming data on the RX line
    Unit.scanf("%s,",Data);

    // translate the data from ASCII into int/float native types
    // numbers in brackets is where the data starts in the string
    deviceId = Ascii2Char(0);
    Mode = Ascii2Char(3);
    TempReading = Ascii2Float(6);
    x = Ascii2Char(15);
    battReading = (float)x/10;

    // print the original unprocessed input string
    pc.printf("%s,\n\r",Data);

    // print what we translated
    pc.printf("Device ID = %i\n\r", deviceId);
    pc.printf("Mode = %i\n\r", Mode);
    pc.printf("Temp = %.1f\n\r", TempReading);
    pc.printf("Bat = %.1f\n\n\r", battReading);
}

如果在启动时刷新传入数据流(读取并丢弃任何现有的缓冲垃圾),您也可能会获得更好的结果,这可能会解决您第一次读取时的额外字符问题。但是如果你的通信两端之间存在启动竞争条件link,你可能会发现(可能偶尔)你的接收器开始处理数据包字符中间的第一个样本,并且可能那个刷新操作会丢弃数据包的第一部分。虽然初始刷新是个好主意,但更重要的是您有一种可靠的方法来验证每个数据包。

这是对您的情况的补充评论:

在他的评论中,MartinJames 是正确的,尽管可能有点直率。众所周知,没有明确定义的数据包协议的串行数据流是不可靠的,通过此类接口记录数据很可能会产生错误数据,如果您正在对结果数据集进行研究或工程设计,这可能会产生严重后果。一个更健壮的消息系统可能会以已知字符或字符对开始每个“数据包”,就像有用的重新同步机制一样:如果您的字节流不同步,重新同步字符(或对)可以帮助您恢复同步快速轻松。在你的情况下,因为你正在阅读 ASCII 数据,那是 '\n'"\r\n",所以从这个角度来看你很好,只要你实际做一些事情来启动和停止那些数据样本界限。如果您收到这样的数据样本会怎样?...

01,03,CDCC1242,28,
01,03,CDCC1240,27,
01,03,CDCC1241,29,
01,03,CDCC1243,28,
01,03,CDCC123F,2A,
01,03,CD9,
01,03,CDCC1241,29,
01,03,CDCC1241,29,
01,0yĔñvśÄ“3,CDCC1243,28,
01,03,CDCC123F,2A,
01,03,CDCC1242,29,

您的代码能否在缺少多个字符的示例后重新同步?那里面有垃圾的呢?您的代码需要能够将串行流分成以一个分隔符(或一对)开始并在串行流中的下一个分隔符之前结束的块。它应该检查之间的字符并以某种方式验证它们“有意义”,并能够拒绝任何未检查为 OK 的样本。然后它的作用可能取决于您的数据最终消费者的需求:也许您可以扔掉样本并仍然可以。或者,也许您应该重复上一个好的样本,直到找到下一个好的样本。或者,也许您应该等到下一个好的样本,然后进行线性插值,以找到这些好的样本之间的数据应该是什么的合理估计值。

无论如何,正如我所说,您需要一些方法来验证每个数据样本。如果“数据包”(数据样本)长度可以变化,那么每个数据包都应该包含一些关于其中字节数的指示,所以如果你得到或多或少,你就知道这个数据包是坏的。 (此外,如果长度不合理,您也知道数据是错误的,并且您不会让您的数据收集算法被错误字节所愚弄,即您的下一个数据包长 1.8 GB……这可能会使您的程序崩溃,因为您的接收缓冲区没有那么大。)最后,数据包中的所有数据都应该有某种校验和系统; 16 位附加校验和可以工作,但 CRC 会更好。通过在发送端生成此数据包开销元数据并在接收端对其进行验证,您(至少在一定程度上)保证了数据集的有效性。

但是正如您所说,您无法控制传输数据的格式。真可惜;正如 MartinJames 所说,设计该协议的人似乎并不了解简单串行字节流的不可靠性。由于您无法更改它,因此您只需要尽最大努力找到一些启发式方法来验证数据;也许你让你的代码记住数组中的最后 5 个样本,并将每个新样本与最后 5 个假设有效的样本进行比较;如果你从前面的样本中得到一个超出合理变化范围的值,你就把它扔掉并等待下一个。或者想出你自己的启发式方法。如果实际测量值变化太快,请确保您的启发式方法不会导致您使所有未来样本无效。