本帖最后由 机器人爱好者1991 于 2023-10-12 18:01 编辑
为了便于自己debug,自己编写了一个c语言版本的基于控制台的上位机。
一、功能
- 上位机发送北京时间到单片机。
- 上位机捕获键盘的输入,发送速度指令到单片机。其中W按键表示前进,S按键代表后退,A表示左转,D表示右转,其他代表暂停;按键的捕获设置1s的时间超时。
- 单片机返回数据到上位机。
- 加载配置文件,配置文件里面是串口的编号和波特率。
二、数据协议
为了保证数据传输的正确性,增加了帧头帧尾以及CRC16校验码。
帧头 |
命令(读写) |
操作码 |
数据长度 |
low |
high |
low |
high |
crc16_low |
crc16_low |
帧尾 |
0x55 |
0xaa |
0x06 |
0x02 |
0x04 |
0x64 |
0x00 |
0x64 |
0x00 |
0x35 |
0xd4 |
0x0a |
0x0d |
帧头用于标识数据包的开始,并帮助接收方正确解析数据。0x55的二进制表示为 01010101,0xaa的二进制表示为 10101010。出现问题排查的时候,01的交替可以方便排查问题。
帧尾通常设置为0x0a和0x0d是因为它们分别代表换行符(\n)和回车符(\r)。在通信协议中,帧尾用于标识数据包的结束。通过将帧尾设置为0x0a和0x0d,接收方可以检测到数据包的结束,并进行相应的处理。这样可以确保数据包的完整性和正确解析。此外,0x0a和0x0d也被广泛用于文本文件中的换行和回车操作。
命令(读写)就使用0x03代表读取,0x06代表写入。操作码0x01代表时间,操作码0x02代表速度的操作。数据长度位表示从当前位往后数多少位,是数据的有效部分。
CRC16(循环冗余校验)是一种根据数据内容计算校验值的算法,可用于检测数据传输过程中的错误或损坏。首先,需要准备要进行校验的数据。将 CRC 寄存器初始化为一个特定的初始值,通常为0xFFFF。然后,逐位处理数据,将当前数据位与 CRC 寄存器的最高位进行异或操作,然后将 CRC 寄存器向左移动一位。如果 CRC 寄存器的最高位为1,执行异或操作,并使用预定义的多项式(如0x8005)进行异或。继续处理下一个数据位,重复上述步骤,直到所有数据位都处理完毕。最后,CRC 寄存器中存储的值即为 CRC16 校验码。通过将计算得到的 CRC16 校验码与接收到的校验码进行比较,可以判断数据传输是否存在错误。
三、代码流程
- 加载配置文件,配置要打开的串口编号。void load_config(char *config_file, char *port_name, int *baud_rate) 该函数允许从配置文件中加载端口名和波特率,如果配置文件不存在,则使用默认值。
port_name=/dev/ttyUSB0
baud_rate=115200
打开文件,使用 fopen 函数尝试打开指定的配置文件。逐行读取文件内容,使用 fgets 函数逐行读取配置文件的内容。解析配置项,使用 strncmp 函数检查每一行是否包含"port_name="或"baud_rate="的配置项。提取配置值,使用 strtok 函数提取配置值,去除换行符。处理"port_name=":如果是"port_name="的配置项,将提取的值复制到传入的 port_name 参数中。处理"baud_rate=":如果是"baud_rate="的配置项,将提取的值转换为整数并存储到传入的 baud_rate 参数中。关闭文件,使用 fclose 函数关闭打开的文件。处理文件打开失败,如果文件打开失败,使用默认值 /dev/ttyUSB0 和 115200。
- 打开串口。代码用于打开串口设备并配置其通信参数。它是一个跨平台的函数,根据操作系统的不同使用不同的API来操作串口设备。因为我的电脑有的时候是win,上班摸鱼的时候是ubuntu,所以得设置成跨平台的。
首先,代码通过条件编译(#ifdef _WIN32)来区分操作系统是否为Windows。如果是Windows操作系统,使用CreateFileA函数打开串口设备,以port_name为串口设备的名称,以读写方式打开。如果无法打开串口设备,将输出错误信息并返回false。获取当前串口设备的属性,并设置波特率(BaudRate)、数据位数(ByteSize)、停止位(StopBits)和校验位(Parity)等通信参数。如果无法设置串口属性,将输出错误信息并关闭串口设备,并返回false。设置串口的读写超时时间。如果无法设置超时时间,将输出错误信息并关闭串口设备,并返回false。如果一切正常,返回true表示串口打开和配置成功。
如果不是Windows操作系统,代码执行以下步骤。使用open函数打开串口设备,以port_name为串口设备的路径,以读写方式打开。如果无法打开串口设备,将输出错误信息并返回false。获取当前串口设备的属性,并设置波特率(BaudRate)、数据位数(ByteSize)、停止位(StopBits)、校验位(Parity)和流控制(CRTSCTS)等通信参数。如果无法设置串口属性,将输出错误信息并关闭串口设备,并返回false。设置串口的输入输出模式,禁用规范模式(ICANON)和本地回显(ECHO)等。设置串口的输入控制模式,禁用软件流控制(IXON和IXOFF)。设置串口的输入字符处理,禁用回车转换(INLCR)和换行转换(IGNCR)等。设置串口的输出字符处理,禁用回车转换(ONLCR)和换行转换(OCRNL)。设置串口的读取等待时间(VTIME)和最小字符数(VMIN)。清空串口的输入缓冲区。如果无法设置串口属性,将输出错误信息并关闭串口设备,并返回false。如果一切正常,返回true表示串口打开和配置成功。
- 串口发送。write_serial_port 的函数,用于向串口写入数据。它根据操作系统的不同进行了不同的实现。 在Windows平台( _WIN32 宏定义下),使用 WriteFile 函数来写入数据到串口。 WriteFile 函数的参数包括串口句柄( serial_port )、数据缓冲区指针( data )、数据长度( length )以及用于存储实际写入字节数的变量( bytes_written )。如果 WriteFile 函数返回失败,则打印错误消息并返回 false 表示写入失败。 在其他操作系统平台,使用 write 函数来写入数据到串口。 write 函数的参数包括串口文件描述符( serial_port )、数据缓冲区指针( data )和数据长度( length )。如果 write 函数返回的写入字节数小于0,则打印错误消息并返回 false 表示写入失败。最后,如果写入成功,函数返回 true 表示写入成功。
- 串口接收。这段代码的作用是从串口读取数据,并将读取的数据存储在指定的缓冲区中。函数名为read_serial_port,接受两个参数:一个是指向缓冲区的指针,另一个是缓冲区的长度。代码中使用了条件编译,根据操作系统的不同选择不同的读取函数。对于Windows系统,使用ReadFile函数从串口读取数据;对于其他系统,使用read函数从串口读取数据。如果读取数据失败,则返回false或者-1,具体取决于操作系统。如果成功读取数据,则返回读取到的字节数。
- 键盘输入。
这段代码定义了一个名为 get_user_input_with_timeout 的函数,用于从用户输入中获取字符,并设置了超时时间。
首先,根据操作系统的不同,使用不同的方法获取标准输入的句柄(Windows使用 GetStdHandle 函数,其他系统使用 tcgetattr 函数)。 接下来,根据操作系统的不同,设置输入模式。在Windows中,使用 SetConsoleMode 函数禁用回显和行输入。在其他系统中,使用 tcgetattr 函数获取当前终端设置,然后通过修改 c_lflag 字段禁用规范模式和回显,设置 VTIME 字段为超时时间的十分之一,设置 VMIN 字段为0以启用非阻塞模式,最后使用 tcsetattr 函数将新的设置应用到终端。 声明一个字符变量 command ,用于存储用户输入的字符。 根据操作系统的不同,使用不同的方法从标准输入中读取一个字符。在Windows中,使用 ReadFile 函数读取字符,并检查读取的字节数是否大于0。在其他系统中,使用 read 函数读取字符,并检查返回值是否大于0。 如果成功读取到字符,恢复之前的输入模式,并返回读取到的字符。 如果读取失败或超时,同样恢复输入模式,并返回空字符( \0 )。
需要注意的是,这段代码中使用了一些平台特定的函数和宏,如 GetStdHandle 、 SetConsoleMode 、 tcgetattr 、 tcsetattr 、 read 、 STD_INPUT_HANDLE 、 ENABLE_ECHO_INPUT 、 ENABLE_LINE_INPUT 、 ICANON 、 ECHO 、 VTIME 、 VMIN 等。这些函数和宏的定义需要根据具体的操作系统和编译环境进行适配和调整。
- CRC-16校验。用于检测数据传输过程中是否发生错误或数据损坏。函数名为calculate_crc,接受四个参数:一个是指向数据的指针,一个是数据的长度,另外两个是指向CRC校验结果的指针(低字节和高字节)。首先,将CRC初始值设置为0xFFFF。然后,对于数据中的每个字节,将其与CRC进行异或操作。接着,对于每个字节的每个位,判断当前位是否为1。如果是1,则右移CRC一位,并与0xA001进行异或操作;如果是0,则只右移CRC一位。最后,将计算得到的CRC值分别存储到crc_low和crc_high中,其中crc_low存储CRC的低字节,crc_high存储CRC的高字节。
- main函数。 定义了一个字符数组 port_name 和一个整数 baud_rate ,用于存储串口名称和波特率。 调用load_config 函数从配置文件中加载串口名称和波特率。 使用 printf 函数打印出加载的串口名称和波特率。 调用 open_serial_port 函数打开串口,如果打开失败则返回-1。 创建一个线程来接收串口数据。 进入一个无限循环,不断读取用户输入的命令。 使用 get_user_input_with_timeout 函数获取用户输入的字符,并根据输入的命令执行相应的操作。 在 switch 语句中,根据不同的输入命令执行不同的操作,如前进、后退、左转、右转等。 在每次循环中,调用 get_current_time 函数获取当前时间并发送数据。 最后,关闭串口并返回0。
四、编译
我是在ubuntu2004上编译生成的win使用的exe,使用x86_64-w64-mingw32-gcc。socat来模拟串口。下面两张图是在ubuntu2004和win10简单调试的截图。
sudo apt-get install mingw-w64
x86_64-w64-mingw32-gcc -o serial.exe serial.c -lpthread
gcc -o serial serial.c -lpthread
sudo apt-get install socat
socat -d -d pty,raw,echo=0 pty,raw,echo=0
五、代码
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#endif
#ifdef _WIN32
#define DEFAULT_BAUD_RATE CBR_115200
#else
#define DEFAULT_BAUD_RATE B115200
#endif
#ifdef _WIN32
HANDLE serial_port;
#else
int16_t serial_port;
#endif
// 定义帧头和帧尾
#define frame_header1 0x55
#define frame_header2 0xAA
#define frame_footer1 0x0A
#define frame_footer2 0x0D
bool open_serial_port(int8_t *port_name, int16_t baud_rate)
{
#ifdef _WIN32
serial_port = CreateFileA(port_name, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (serial_port == INVALID_HANDLE_VALUE)
{
printf("无法打开串口设备\n");
return false;
}
DCB dcbSerialParams = {0};
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
if (!GetCommState(serial_port, &dcbSerialParams))
{
printf("无法获取串口属性\n");
CloseHandle(serial_port);
return false;
}
dcbSerialParams.BaudRate = baud_rate;
dcbSerialParams.ByteSize = 8;
dcbSerialParams.StopBits = ONESTOPBIT;
dcbSerialParams.Parity = NOPARITY;
if (!SetCommState(serial_port, &dcbSerialParams))
{
printf("无法设置串口属性\n");
CloseHandle(serial_port);
return false;
}
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = 50;
timeouts.ReadTotalTimeoutConstant = 50;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 50;
timeouts.WriteTotalTimeoutMultiplier = 10;
if (!SetCommTimeouts(serial_port, &timeouts))
{
printf("无法设置串口超时时间\n");
CloseHandle(serial_port);
return false;
}
return true;
#else
serial_port = open(port_name, O_RDWR | O_NOCTTY | O_NDELAY);
if (serial_port < 0)
{
printf("无法打开串口设备\n");
return false;
}
struct termios tty;
memset(&tty, 0, sizeof(tty));
if (tcgetattr(serial_port, &tty) != 0)
{
printf("无法获取串口属性\n");
close(serial_port);
return false;
}
cfsetospeed(&tty, baud_rate);
cfsetispeed(&tty, baud_rate);
tty.c_cflag |= CLOCAL;
tty.c_cflag |= CREAD;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CRTSCTS;
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_iflag &= ~(IXON | IXOFF | IXANY);
tty.c_iflag &= ~(INLCR | IGNCR | ICRNL);
tty.c_oflag &= ~(ONLCR | OCRNL);
tty.c_cc[VTIME] = 0;
tty.c_cc[VMIN] = 1;
tcflush(serial_port, TCIFLUSH);
if (tcsetattr(serial_port, TCSANOW, &tty) != 0)
{
printf("无法设置串口属性\n");
close(serial_port);
return false;
}
return true;
#endif
}
void close_serial_port()
{
#ifdef _WIN32
CloseHandle(serial_port);
#else
close(serial_port);
#endif
}
bool write_serial_port(uint8_t *data, int16_t length)
{
#ifdef _WIN32
DWORD bytes_written;
if (!WriteFile(serial_port, data, length, &bytes_written, NULL))
{
printf("写入串口数据失败\n");
return false;
}
#else
int16_t bytes_written = write(serial_port, data, length);
if (bytes_written < 0)
{
printf("写入串口数据失败\n");
return false;
}
#endif
return true;
}
int16_t read_serial_port(uint8_t *buffer, int16_t length)
{
#ifdef _WIN32
DWORD bytes_read;
if (!ReadFile(serial_port, buffer, length, &bytes_read, NULL))
{
printf("读取串口数据失败\n");
return false;
}
#else
int16_t bytes_read = read(serial_port, buffer, length);
if (bytes_read < 0)
{
// printf("读取串口数据失败\n");
return -1;
}
#endif
return bytes_read;
}
void calculate_crc(const uint8_t *data, int16_t length, uint8_t *crc_low, uint8_t *crc_high)
{
uint16_t crc = 0xFFFF;
for (int16_t i = 0; i < length; i++)
{
crc ^= data[i];
for (int16_t j = 0; j < 8; j++)
{
if (crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
}
*crc_low = crc & 0xFF;
*crc_high = crc >> 8;
}
void get_current_time()
{
uint8_t write_data[21];
uint8_t crc_low, crc_high;
int16_t send_index = 0;
write_data[send_index++] = frame_header1;
write_data[send_index++] = frame_header2;
write_data[send_index++] = 0x06; // 写入
write_data[send_index++] = 0x01; // 时间
write_data[send_index++] = 12; // 数据长度
time_t now = time(NULL);
struct tm *timeinfo = localtime(&now);
// 将年、月、日、时、分、秒按小端格式存储到数组中
write_data[send_index++] = (timeinfo->tm_year + 1900) & 0xFF;
write_data[send_index++] = (timeinfo->tm_year + 1900) >> 8;
write_data[send_index++] = (timeinfo->tm_mon + 1) & 0xFF;
write_data[send_index++] = (timeinfo->tm_mon + 1) >> 8;
write_data[send_index++] = timeinfo->tm_mday & 0xFF;
write_data[send_index++] = timeinfo->tm_mday >> 8;
write_data[send_index++] = timeinfo->tm_hour & 0xFF;
write_data[send_index++] = timeinfo->tm_hour >> 8;
write_data[send_index++] = timeinfo->tm_min & 0xFF;
write_data[send_index++] = timeinfo->tm_min >> 8;
write_data[send_index++] = timeinfo->tm_sec & 0xFF;
write_data[send_index++] = timeinfo->tm_sec >> 8;
// 计算CRC16校验
calculate_crc(&write_data[5], write_data[4], &crc_low, &crc_high);
// 将CRC校验值赋值给write_data[16]和write_data[17]
write_data[send_index++] = crc_low;
write_data[send_index++] = crc_high;
write_data[send_index++] = frame_footer1;
write_data[send_index++] = frame_footer2;
// 发送数据
if (!write_serial_port(write_data, send_index))
{
printf("发送数据失败\n");
}
for (int16_t i = 0; i < send_index; i++)
{
printf(" %02X ", write_data[i]);
}
printf("\n");
}
void write_velocity_cmd(const int16_t left_velocity, const int16_t right_velocity)
{
uint8_t write_data[21];
uint8_t crc_low, crc_high;
int16_t send_index = 0;
write_data[send_index++] = frame_header1;
write_data[send_index++] = frame_header2;
write_data[send_index++] = 0x06; // 写入
write_data[send_index++] = 0x02; // 速度
write_data[send_index++] = 4; // 数据长度
write_data[send_index++] = left_velocity & 0xFF;
write_data[send_index++] = left_velocity >> 8;
write_data[send_index++] = right_velocity & 0xFF;
write_data[send_index++] = right_velocity >> 8;
// 计算CRC16校验
calculate_crc(&write_data[5], write_data[4], &crc_low, &crc_high);
// 将CRC校验值赋值给write_data[16]和write_data[17]
write_data[send_index++] = crc_low;
write_data[send_index++] = crc_high;
write_data[send_index++] = frame_footer1;
write_data[send_index++] = frame_footer2;
// 发送数据
if (!write_serial_port(write_data, send_index))
{
printf("发送数据失败\n");
}
for (int16_t i = 0; i < send_index; i++)
{
printf(" %02X ", write_data[i]);
}
printf("\n");
}
/*解析返回的速度*/
void read_velocity_cmd(const uint8_t *buffer, const int16_t length, int16_t *left_velocity, int16_t *right_velocity)
{
if (0x06 != buffer[2] || 0x02 != buffer[3])
{
return;
}
*left_velocity = (int16_t *)(buffer[5] + ((int16_t)(buffer[6]) << 8));
*right_velocity = (int16_t *)(buffer[7] + ((int16_t)(buffer[8]) << 8));
printf("left_velocity=%d\n,right_velocity=%d\n", *left_velocity, *right_velocity);
}
/*解析返回的时间*/
void read_time_cmd(const uint8_t *buffer, const int16_t length, int16_t *year, int16_t *month, int16_t *day,
int16_t *hour, int16_t *minute, int16_t *second)
{
if (0x06 != buffer[2] || 0x01 != buffer[3])
{
return;
}
*year = (int16_t *)(buffer[5] + ((int16_t)(buffer[6]) << 8));
*month = (int16_t *)(buffer[7] + ((int16_t)(buffer[8]) << 8));
*day = (int16_t *)(buffer[9] + ((int16_t)(buffer[10]) << 8));
*hour = (int16_t *)(buffer[11] + ((int16_t)(buffer[12]) << 8));
*minute = (int16_t *)(buffer[13] + ((int16_t)(buffer[14]) << 8));
*second = (int16_t *)(buffer[15] + ((int16_t)(buffer[16]) << 8));
printf("year=%d,month=%d,day=%d,hour=%d,minute=%d,second=%d\n", *year, *month, *day, *hour, *minute, *second);
}
void load_config(int8_t *config_file, int8_t *port_name, int32_t *baud_rate)
{
FILE *file = fopen(config_file, "r");
if (file != NULL)
{
int8_t line[256];
while (fgets(line, sizeof(line), file))
{
if (strncmp(line, "port_name=", 10) == 0)
{
int8_t *value = strtok(line + 10, "\n");
strncpy(port_name, value, strlen(value));
}
else if (strncmp(line, "baud_rate=", 10) == 0)
{
int8_t *value = strtok(line + 10, "\n");
*baud_rate = atoi(value);
}
}
fclose(file);
}
else
{
// 配置文件不存在,默认使用默认值
strncpy(port_name, "/dev/ttyUSB0", sizeof(port_name));
*baud_rate = 115200;
}
}
int8_t get_user_input_with_timeout(int16_t timeout)
{
#ifdef _WIN32
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD fdwMode, fdwOldMode;
GetConsoleMode(hStdin, &fdwOldMode);
fdwMode = fdwOldMode & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT);
SetConsoleMode(hStdin, fdwMode);
#else
struct termios old_settings, new_settings;
tcgetattr(STDIN_FILENO, &old_settings);
new_settings = old_settings;
new_settings.c_lflag &= ~(ICANON | ECHO); // 禁用回显和缓冲
new_settings.c_cc[VTIME] = timeout * 10; // 设置输入超时时间(以0.1秒为单位)
new_settings.c_cc[VMIN] = 0; // 设置非阻塞模式
tcsetattr(STDIN_FILENO, TCSANOW, &new_settings);
#endif
int8_t command;
#ifdef _WIN32
DWORD dwRead;
if (ReadFile(hStdin, &command, sizeof(command), &dwRead, NULL) && dwRead > 0)
#else
if (read(STDIN_FILENO, &command, sizeof(command)) > 0)
#endif
{
#ifdef _WIN32
SetConsoleMode(hStdin, fdwOldMode);
#else
tcsetattr(STDIN_FILENO, TCSANOW, &old_settings);
#endif
return command;
}
#ifdef _WIN32
SetConsoleMode(hStdin, fdwOldMode);
#else
tcsetattr(STDIN_FILENO, TCSANOW, &old_settings);
#endif
return '\0';
}
void parse_serial_data(const uint8_t *buffer, const int16_t length)
{
uint8_t crc_low = 0, crc_high = 0;
int16_t left_velocity = 0, right_velocity = 0;
int16_t year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0;
// 帧头帧尾的校验
if (frame_header1 != buffer[0] || frame_header2 != buffer[1] || frame_footer1 != buffer[length - 2] ||
frame_footer2 != buffer[length - 1])
{
return;
}
calculate_crc(&buffer[5], buffer[4], &crc_low, &crc_high);
if (crc_low != buffer[length - 4] || crc_high != buffer[length - 3])
{
printf("CRC校验不通过!\n");
return;
}
printf("接收到的数据(长度:%d):", length);
for (int16_t i = 0; i < length; i++)
{
printf("%02X ", buffer[i]);
}
printf("\n");
read_velocity_cmd(buffer, length, &left_velocity, &right_velocity);
read_time_cmd(buffer, length, &year, &month, &day, &hour, &minute, &second);
}
// 新增的线程函数,用于接收串口数据
void *receive_serial_data(void *arg)
{
uint8_t buffer[256];
while (1)
{
int16_t read_len = read_serial_port(buffer, sizeof(buffer));
if (read_len > 0)
{
parse_serial_data(buffer, read_len);
}
}
return NULL;
}
int16_t main()
{
int8_t port_name[256];
int32_t baud_rate;
load_config("config.txt", port_name, &baud_rate);
printf("Port name: %s\n", port_name);
printf("Baud rate: %d\n", baud_rate);
if (!open_serial_port(port_name, baud_rate))
{
return -1;
}
// 创建线程来接收串口数据
pthread_t thread;
pthread_create(&thread, NULL, receive_serial_data, NULL);
while (1)
{
int8_t command = get_user_input_with_timeout(1);
switch (command)
{
case 'w':
// 执行前进命令
printf("go ahead\n");
write_velocity_cmd(100, 100);
break;
case 's':
// 执行后退命令
printf("go back\n");
write_velocity_cmd(-100, -100);
break;
case 'a':
// 执行左转命令
printf("turn left\n");
write_velocity_cmd(-100, 100);
break;
case 'd':
// 执行右转命令
printf("turn right\n");
write_velocity_cmd(100, -100);
break;
case 'q':
// 退出程序
printf("exit\n");
close_serial_port();
return 0;
default:
printf("stop\n");
write_velocity_cmd(0, 0);
break;
}
// 获取当前时间并发送数据
get_current_time();
// 延时一段时间
}
close_serial_port();
return 0;
}