为了便于自己debug,自己编写了一个c语言版本的基于控制台的上位机。
一、功能
二、数据协议
为了保证数据传输的正确性,增加了帧头帧尾以及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 校验码与接收到的校验码进行比较,可以判断数据传输是否存在错误。
三、代码流程
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。
四、编译
我是在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;
}
本帖最后由 机器人爱好者1991 于 2023-10-12 18:01 编辑
import serial
# 串口配置
port = "/dev/pts/7"
baud_rate = 115200
# 打开串口
ser = serial.Serial(port, baud_rate)
# 准备要发送的数据(20个字节)
data_hex = "55AA0602046400640035D40A0D"
data_bytes = bytes.fromhex(data_hex)
print(data_hex)
# 发送数据到串口
ser.write(data_bytes)
# 关闭串口
ser.close()
楼主对通信协议还是有自己独到的见解的,同时也有上位的编写能力,非常强!
引用: lugl4313820 发表于 2023-10-10 21:25 楼主对通信协议还是有自己独到的见解的,同时也有上位的编写能力,非常强!
哈哈,谢谢支持。leader就在我侧后方,我就打开vscode,然后使用虚拟串口测试通信。然后移植。
您这个厉害了
全才呀
引用: jobszheng5 发表于 2023-10-11 09:21 您这个厉害了 全才呀
谢谢,您过奖,我这也是边查阅资料边写的程序和文章。
引用: ddllxxrr 发表于 2023-10-12 06:07 跨了两大操作系统,有点高难。
我是查资料然后写的,时间仓促,还有不足,有空再整UI的。今天测试了下win的程序,能用。就是不按按键不发数据。有空再看,其他的达到要求了。