在 Arduino 中处理串行线路数据





5.00/5 (3投票s)
解释如何可靠地从 Arduino 的串行线路读取数据或命令,
引言
在编写 Arduino 程序时,有时需要从串行线路接收一些命令或数据。这看起来可能很容易,但你经常会遇到问题。让我用一个例子来解释我的意思。你有一个程序正在做很多事情——读取传感器、控制输出、在显示器上显示当前状态等。你让你的循环代码快速运行,例如,它每秒执行10次。现在你希望你的程序响应从串行线路发送的命令或处理从另一个 Arduino 发送的数据。你如何做到这一点?
好吧,你将 if (Serial.available() > 0)
放入你的循环中,如果有数据(或命令),你就处理它。但问题在这里开始显现。如果有一个或多个字符可用,则条件为真。如果你的数据或命令长度超过1个字符,很有可能你还无法处理它,因为它还没有完全接收。可能只有第一个字符,或前两个字符等。一个解决方案是等待所有数据到达;例如调用 Serial.readStringUntil
,它将读取直到收到终止字符或超时。但你不能停止循环并等待!你希望每秒处理输入和控制输出10次——想象一下你的程序控制着一架四轴飞行器;它不能停止控制飞行器并等待数据从串行线路到达。
另一个解决方案是将条件更改为类似以下内容
if ( Serial.available() == 3 )
Process the data….
在这段代码中,我假设数据/命令长度为3个字符,并且我只在收到恰好3个字符时才进行处理。看起来不错?并非总是如此。让我给你看一个例子
假设一个 Arduino 每秒发送一次数字(固定3位长度)。我们另一个 Arduino 中的程序正在接收这些数字,并且它需要每秒运行10次循环来控制你地下室的核反应堆。以下是串行线路可能出现的 char
序列:
001 002 003 004.
现在,如果接收程序“同步”——如果它从头开始读取,它将正确接收数字。但想象一下,例如,接收 Arduino 断电然后重新上电。它可能会在序列中的任何随机位置开始接收。例如,它接收到的第一个 char
可能是“2
”,然后当我们收到“200
”而不是“002
”或“003
”时,条件 if (Serial.available() == 3)
将为 true
——一个无效的数字。
如果发送方是人而不是另一个 Arduino,情况可能会更糟。可能会有无效命令,每个 char
之间有随机空格等。此外,你可能不想将命令限制为固定长度。所以这个解决方案也远非理想。
注意:Arduino IDE 中的串行监视器可能会以某种方式隐藏这些问题,因为它允许用户输入整个 string
(命令),然后通过“发送”按钮发送。因此,串行线路上的数据是以短脉冲的形式出现的,例如“command1 command2
”,而不是“c o m m a n d 1 c o m
”等,这会在用户输入字符后立即发送(正常终端程序就是这样做的)。无论如何,我相信你希望你的程序是健壮的,并能很好地处理所有情况。
因此,我在这里提出一个解决方案,它应该能够处理随机到达的字符,并在真正准备好时处理命令或数据,而不会长时间阻塞循环。
Using the Code
第一个示例处理来自用户的命令。命令最长可达 MAX_DATA_LEN
个 char
,并且以不阻塞 loop()
函数的方式接收。代码不作任何假设,例如程序在用户开始输入时总是会接收字符。
重要提示:在 Arduino 串行监视器中测试程序时,将其设置为在窗口底部发送回车符,而不是默认的“无行结束”。程序期望命令以回车键(回车符)终止。
关于 String 类
在代码中,我使用 C 语言的 string
——即 char
数组。C 语言中没有 string
的数据类型。可以在 C++ 中使用 string
类,并且 Arduino 中有一个 String
类,允许你像 C# 或 Java 等语言一样使用 string
,但我不建议使用这个类。首先,我读到了一些关于其效率低下、内存泄漏等的不好的消息,其次,即使这个类编写得很好,与 C 语言的 string
处理相比,它仍然会效率低下,因为它在执行 myString = myString + “ you too!”;
之类的操作时需要动态分配和释放内存。在 Arduino 等嵌入式系统上使用动态内存,有时甚至是 C++,并不是一个好主意,正如我在这篇文章中试图解释的那样 我的文章。
我知道使用 C 风格的 string
看起来很吓人,但实际上它相当容易,一旦你学会了,你就能编写高效的代码,并用你收到的字符做所有酷炫的技巧。
程序功能
下面的示例代码从串行线路接收命令并执行它们。支持以下命令
ledon
打开引脚 13 上的板载 LEDledoff
关闭板载 LEDver
打印程序版本。
在 loop()
中,有 delay(100);
模拟程序应该做的一些有用的工作,例如读取传感器、处理输入等。然后是 processSerialData
函数,它负责处理从串行线路接收到的命令。它不会长时间阻塞程序,只是存储自上次调用以来接收到的字符,如果接收到完整的命令,它就会执行。代码在下面有更详细的解释。
// Processing serial data for arduino
// Support commands:
// ledon - turn on LED on pin 13
// ledoff - turn off LED on pin 13
// ver - print program version
// Commnands must be terminated by Enter (return) - enable this in Arduino
// serial monitor - set "Carriage return" instead on "No line ending".
// Or change the TERMINATOR_CHAR in the code below.
#include <string.h> // for strcmp()
// max length of a the data or command
#define MAX_DATA_LEN (8)
#define TERMINATOR_CHAR ('\r')
// the buffer for the received chars
// 1 extra char for the terminating character "\0"
char g_buffer[MAX_DATA_LEN + 1];
// function prototype
void processSerialData(void);
void setup() {
// put your setup code here, to run once:
pinMode(13, OUTPUT);
Serial.begin(9600);
}
void loop() {
// Here we read the sensors, calculate the output etc.
delay(100);
// Here we process data from serial line
processSerialData();
}
void processSerialData(void) {
int data;
bool dataReady;
while ( Serial.available() > 0 ) {
data = Serial.read();
dataReady = addData((char)data);
if ( dataReady )
processData();
}
}
// Put received character into the buffere.
// When a complete command is received return true, otherwise return false.
// The command is terminated by Enter character ("\r")
bool addData(char nextChar)
{
// This is position in the buffer where we put next char.
// Static var will remember its value across function calls.
static uint8_t currentIndex = 0;
// Ignore some characters - new line, space and tabs
if ((nextChar == '\n') || (nextChar == ' ') || (nextChar == '\t'))
return false;
// If we receive Enter character...
if (nextChar == TERMINATOR_CHAR) {
// ...terminate the string by NULL character "\0" and return true
g_buffer[currentIndex] = '\0';
currentIndex = 0;
return true;
}
// For normal character just store it in the buffer and move
// position to next
g_buffer[currentIndex] = nextChar;
currentIndex++;
// Check for too many chars
if (currentIndex >= MAX_DATA_LEN) {
// The data too long so reset our position and return true
// so that the data received so far can be processed - the caller should
// see if it is valid command or not...
g_buffer[MAX_DATA_LEN] = '\0';
currentIndex = 0;
return true;
}
return false;
}
// process the data - command
// strcmp compares two strings and returns 0 if they are the same.
void processData(void)
{
if ( strcmp(g_buffer, "ledon") == 0 ) {
digitalWrite(13, HIGH);
Serial.println("LED1 is on");
}
else if (strcmp(g_buffer, "ledoff") == 0 ) {
digitalWrite(13, LOW);
Serial.println("LED1 is off");
}
else if (strcmp(g_buffer, "ver") == 0 ) {
Serial.println("Version 1.0");}
else {
Serial.print("Unknown command ");
Serial.println(g_buffer);
}
}
代码解释
processSerialData
函数从串行线路读取所有可用字符,并为每个字符调用 addData
()。addData
函数将新 char
放入缓冲区,并决定是否接收到完整命令。为此,它只是检查新 char
是否是特殊终止符 TERMINATOR_CHAR
——在代码中,我使用回车键 ('\r'
),但你可以将其更改为其他字符。
字符存储在 C 风格的 string
中,即 char
变量数组。关于 C 字符串,一个重要的事情是 string
的末尾由一个特殊字符标记,写为 '\0
',称为 NULL
字符——这可能是一个令人困惑的名称,但不要担心它,只需确保如果你以逐个 char
的方式创建 string
,你总是将 '\0
' 放在 string
的末尾——就像 addData
函数所做的那样。如果你将 string
写为双引号中的 char
,例如 processData
函数中的 "ledon"
string
,则不需要添加 '\0
'。在这种情况下,编译器会自动添加终止符。但请务必确保你的数组中有空间用于此终止符——请参阅 g_buffer
如何声明为 +1,以允许存储 MAX_DATA_LEN
个可用 char
。
因此,addData
函数将每个字符存储到全局字符串(char
数组)g_buffer
中的一个位置,然后该位置会增加,以便下一个 char
存储为数组中的下一个元素。
如果收到完整的命令,则调用 processData
函数。此函数负责执行命令。在示例中,它只是打开/关闭 LED 或打印此程序的版本。为了确定收到哪个命令,函数必须将存储在 g_buffer
中的 string
与支持的命令进行比较。请注意,你不能像 str1 == str2
那样在 C 中比较 string
。你将只比较 string
的地址,而不是内容。你需要使用标准库函数 strcmp
——string
比较。如果 string
相同,它返回 0
。
尝试代码
要尝试它,只需将上面的代码复制粘贴到你的 Arduino 草图中,将其上传到 Arduino 并打开串行监视器。首先,在串行监视器底部的组合框中选择“回车”,以便在你点击“发送”按钮或按下键盘上的“回车”时发送“回车”键。键入 ledon
并按“回车”以打开 Arduino 板上的 LED。如果你输入无效命令,程序应该会告诉你。
从串行线路接收数字
对于那些有兴趣向程序传递一些数字的人,这里是修改后的代码版本,它展示了如何从串行线路接收两个数字。在发送数字时,你需要定义一些协议,以确保接收程序识别这些数字并且不会将它们混淆——如上面发送 001 002
等的示例中解释的那样。在此程序中,我使用人类友好的表示法,但你可以轻松更改它以满足你的需求。因此,在我的程序中,输入预期为这种格式:“x=10y=20#
”。有 x=
和 y=
来标记数字的开始。
这是代码。要尝试它,请在你的 Arduino 中运行它,并从串行监视器发送,例如 x=10y=20#
。程序应该响应“New params received. x = 10, y = 20
”。
// processing serial data for arduino
// This shows receiving two numbers, the format of the command
// is x=10y=20#
#include <string.h> // for strcmp()
// max length of a the data or command
#define MAX_DATA_LEN (16)
#define TERMINATOR_CHAR ('#')
// the buffer for the received chars
// 1 extra char for the terminating character "\0"
char g_buffer[MAX_DATA_LEN + 1];
// function prototype
void processSerialData(void);
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
}
void loop() {
// Here, we read the sensors, calculate the output, etc.
delay(100);
// Here, we process data from serial line
processSerialData();
}
void processSerialData(void) {
int data;
bool dataReady;
while ( Serial.available() > 0 ) {
data = Serial.read();
dataReady = addData((char)data);
if ( dataReady )
processData();
}
}
// Put received character into the buffere.
// When a complete command is received return true, otherwise return false.
// The command is terminated by Enter character ("\r")
bool addData(char nextChar)
{
// This is position in the buffer where we put next char.
// Static var will remember its value across function calls.
static uint8_t currentIndex = 0;
// Ignore some characters - new line, space and tabs
if ((nextChar == '\n') || (nextChar == ' ') || (nextChar == '\t'))
return false;
// If we receive Enter character...
if (nextChar == TERMINATOR_CHAR) {
// ...terminate the string by NULL character "\0" and return true
g_buffer[currentIndex] = '\0';
currentIndex = 0;
return true;
}
// For normal character just store it in the buffer and move
// position to next
g_buffer[currentIndex] = nextChar;
currentIndex++;
// Check for too many chars
if (currentIndex >= MAX_DATA_LEN) {
// The data too long so reset our position and return true
// so that the data received so far can be processed - the caller should
// see if it is valid command or not...
g_buffer[MAX_DATA_LEN] = '\0';
currentIndex = 0;
return true;
}
return false;
}
// process the data - we expect this format:
// x=10y=20
void processData(void)
{
char* n1pos = strstr(g_buffer, "x=");
int number1 = -1, number2 = -1; // preset invalid values
//int dataLen = strlen(g_buffer); // for checking if there is number after the x or y
// n1pos points to the substring starting with "x=".
// strlen(n1pos) > 2 makes sure there is something after the x=
// but it does not check if there is number... just that the string doesn't end
// right after the = char.
if ( n1pos != NULL && strlen(n1pos) > 2) {
// if first number is found, convert it to integer
// n1pos points to "x=" so we need to add 2 to point
// atoi() to the beginning of the number.
number1 = atoi(n1pos+2);
char* n2pos = strstr(g_buffer, "y=");
if ( n2pos != NULL && strlen(n2pos) > 2 )
number2 = atoi(n2pos+2);
}
if ( number1 >= 0 && number2 >= 0 ) {
Serial.print("New params received. x = ");
Serial.print(number1);
Serial.print(", y = ");
Serial.println(number2);
} else {
Serial.println("Wrong data format, please use x=123y=123");
}
}
代码解释
程序的核心与第一个示例相同。不同之处在于 TERMINATOR_CHAR
设置为 #
,并且 processData
函数现在不同。它尝试解析输入 string
并从中获取两个数字。为此,它使用标准 C 函数 atoi
将 string
转换为数字。如果获取到数字,则将其打印到串行线路。如果未获取到,则打印错误消息。还使用 strstr
函数在接收到的 string
中查找 x=
和 y=
子字符串。
strstr
是一个标准 C 函数,用于在另一个 string
中搜索子字符串。如果未找到 string
,则返回 NULL
;如果找到,则返回指向子字符串开头的指针。我知道“指针”很吓人,但你不需要完全理解它们才能使用 strstr
。如果我只告诉你这一点可能会有所帮助:C 中数组的名称是指向其第一个元素的指针。C 中的 string
是 char
数组。所以 string
的名称实际上是指向 string
开头的指针,你可以将其视为 string
。例如,g_buffer
实际上是指向接收到的 string
的指针。如果你定义
char* p = g_buffer;
p
也指向接收到的 string
。
在代码中,我将 strstr
的结果保存到 char* n1pos
中
char* n1pos = strstr(g_buffer, "x=");
如果 g_buffer
包含 "aaax=12y=10#
" 那么 n1pos
将指向 "x
" 字符。由于指针实际上就像一个 string
,你可以将 n1pos
视为 string
"x=12y=10#
"——从 "x
" 开始的 g_buffer
的子字符串。
你可以向指针添加数字,从而移动 n1pos
所代表的“string
”的开头。我使用 n1pos+2
作为 atoi
函数的输入,以便该函数只接收从“x=
”之后开始的数字。就好像我调用它像 atoi("12y=10#");
一样。数字 12 后面的额外 char
s 不是问题;当 atoi
遇到不是数字的字符时,它将停止转换。
我希望我没有用这两段试图解释指针的文字让事情变得太混乱。如果你想了解更多,请查看网上一些好的教程。
历史
- 2018 年 8 月 6 日:第一个版本