嵌入式 Linux:从串行端口读取字节太慢,无法快速传输
Embedded Linux: Reading bytes from serial port too slow for quicker transfer
我有一个微控制器和一个嵌入式 PC。这两个通过串行连接进行通信,并且都在相互接收和发送数据。这两者之间的波特率为38400。微控制器和PC具有相同的配置以确保通信(8个数据位,1个停止位,偶校验)。
在微控制器开始大约每 10 毫秒发送一次消息之前,通信工作正常。此时,单片机的发送队列已满,超过运行s。然后他向PC发送一条错误消息(我是这样知道的,微控制器的发送队列结束运行,而不是PC的。
在嵌入式 Linux 版本的 PC 程序之前,微控制器 运行 带有 DOS 版本的 PC 程序而不会导致错误。在 DOS 中,单个字节直接从串行端口读取和写入(没有像 Linux 中那样的内核缓冲区)因为大多数 C 代码都可以移植到 Linux 我尝试复制串行的 DOS 行为端口读取和写入 Linux 以保留处理这些单个字节的其余部分。
我在PC中打开并初始化串口如下。
fd_mc = open("/dev/ttyS1", O_NOCTTY | O_RDWR /*| O_NONBLOCK*/ /*| O_SYNC*/);
if(fd_mc == -1)
{
perror("Could not open µc port.");
}
else
{
struct termios tty;
memset(&tty, 0, sizeof(tty));
if ( tcgetattr ( fd_mc, &tty ) != 0 )
{
perror("Error getting termios attributes");
}
cfsetospeed (&tty, B38400);
cfsetispeed (&tty, B38400);
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); //raw input
tty.c_oflag &= ~OPOST; //raw output
tty.c_cflag |= PARENB; //even parity
tty.c_cflag &= ~PARODD;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag |= (CLOCAL | CREAD);
tcsetattr(fd_mc, TCSANOW, &tty);
}
上面的代码是一个函数的片段,它初始化了两个串行端口(其中一个是微控制器的端口)。
编辑:
这里是流量控制的设置,有none.
tty.c_cflag &= ~CRTSCTS; //No hardware based flow control
tty.i_cflag &= ~(IXON | IXOFF | IXANY); //no software based flow control
在环形缓冲区和 pollin 的帮助下,从串行端口读取发生在线程内。该线程在主循环中被调用。代码如下:
void *thread_read()
{
struct sched_param param;
param.sched_priority = 97;
int ret_par = 0;
ret_par = sched_setscheduler(3, SCHED_FIFO, ¶m);
if (ret_par == -1) {
perror("sched_setscheduler");
return 0;
}
struct pollfd poll_fd[2];
int ret;
extern struct fifo mc_fifo, serial_fifo;
ssize_t t;
char c;
poll_fd[0].fd = fd_mc;
poll_fd[0].events = POLLIN;
poll_fd[1].fd = fd_serial;
poll_fd[1].events = POLLIN;
while(1) {
ret = poll(poll_fd, 2, 10000);
if(ret == -1) {
perror("poll");
}
if(poll_fd[0].revents & POLLIN) {
t = read(fd_mc, &c, 1);
if(t>0) {
fifo_in(&mc_fifo, c);
}
}
if(poll_fd[1].revents & POLLIN) {
t = read(fd_serial, &c, 1);
if(t>0) {
fifo_in(&serial_fifo, c);
}
}
}
pthread_exit(NULL);
}
在主循环内调用。
pthread_t read;
pthread_create(&read, NULL, thread_read, NULL);
写入缓冲区(fifo_in)的函数是
int fifo_in(struct fifo *f, char data)
{
if( (f->write + 1 == f->read) || (f->read == 0 && f->write + 1 == FIFO_SIZE) ) //checks if write one before read or at the end
{
printf("fifo in overflow\n");
return 0; //fifo full
}
else {
f->data[f->write] = data;
f->write++;
// printf("Byte in: Containing %4d\tData:\t%4d\n", BytesInReceiveBuffer(1), data); //Bytes contained in fifo of mc
if(f->write >= FIFO_SIZE) {
f->write = 0;
}
return 1; //success
}
}
这个函数基本上做的是检查读写位置在哪里,如果两个位置不重叠并且彼此相距超过+1
,则在缓冲区内写入data
.
当另一个函数需要环形缓冲区内的字节时,它会调用 GetByte
从环形缓冲区中读取字节。
获取字节
int GetByte(int port)
{
char c;
switch(port) {
case 0: //COM1
fifo_out(&serial_fifo,&c);
break;
case 1: //COM2
fifo_out(&mc_fifo,&c);
break;
}
return (int)c;
}
fifo_out
int fifo_out(struct fifo *f, char *data) {
if(f->read == f->write) {
printf("fifo in overfwrite\n");
*data = 0;
return 0;
}
else {
*data = f->data[f->read];
f->read++;
// printf("Byte out: Containing %4d\tData:\t%4d\n", BytesInReceiveBuffer(1), *data);
if(f->read >= FIFO_SIZE) {
f->read = 0;
}
return 1;
}
}
在 Linux 移植之前,DOS 版本中的一切都是顺序的。
吃了那一刻我最好的猜测是,read()
会减慢速度,在某些时候你会开始减慢缓冲区的读取速度,这再次阻止了线程的调用。也许我错了。目前我有点不知道这个错误到底是什么,甚至不知道如何解决这个问题。
感谢每一个好的建议。
您确定在应用程序中需要 fifo 吗?您的串行驱动程序很可能已经在内核中有相当大的缓冲区(通常是一页,通常为 4kB)。如果这就足够了,您可以通过 GetByte
对串行设备进行非阻塞读取来从根本上简化实现。
如果您想坚持这种设计,请考虑修改您的读取循环以一次读取多个字节。现在你需要为每个字节读取两个系统调用。
您一直在更改 PID 3 的调度 class。这可能不是您想要的。此外,这仅意味着一旦字节到达内核内部缓冲区(即 poll
returns 时),您的线程将 运行。如果通过 运行 在工作队列上执行作业将字节从硬件 FIFO 传输到缓冲区,并且该工作队列 运行s 在 SCHED_OTHER
中(大多数人这样做),则更改调度程序你的线程不会有预期的效果。经典的优先级倒置问题。您可能想要审核您的特定板使用的内核驱动程序。不过,如果您在每个 read
清空整个缓冲区,这应该不是什么问题。
如果此代码曾经在 SMP 系统上使用过,您很可能会想用锁来保护 read
和 write
指针,因为它们不是自动更新的。线程很难正确处理,您是否考虑过使用事件循环来代替?类似于 libev.
我有一个微控制器和一个嵌入式 PC。这两个通过串行连接进行通信,并且都在相互接收和发送数据。这两者之间的波特率为38400。微控制器和PC具有相同的配置以确保通信(8个数据位,1个停止位,偶校验)。
在微控制器开始大约每 10 毫秒发送一次消息之前,通信工作正常。此时,单片机的发送队列已满,超过运行s。然后他向PC发送一条错误消息(我是这样知道的,微控制器的发送队列结束运行,而不是PC的。
在嵌入式 Linux 版本的 PC 程序之前,微控制器 运行 带有 DOS 版本的 PC 程序而不会导致错误。在 DOS 中,单个字节直接从串行端口读取和写入(没有像 Linux 中那样的内核缓冲区)因为大多数 C 代码都可以移植到 Linux 我尝试复制串行的 DOS 行为端口读取和写入 Linux 以保留处理这些单个字节的其余部分。
我在PC中打开并初始化串口如下。
fd_mc = open("/dev/ttyS1", O_NOCTTY | O_RDWR /*| O_NONBLOCK*/ /*| O_SYNC*/);
if(fd_mc == -1)
{
perror("Could not open µc port.");
}
else
{
struct termios tty;
memset(&tty, 0, sizeof(tty));
if ( tcgetattr ( fd_mc, &tty ) != 0 )
{
perror("Error getting termios attributes");
}
cfsetospeed (&tty, B38400);
cfsetispeed (&tty, B38400);
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); //raw input
tty.c_oflag &= ~OPOST; //raw output
tty.c_cflag |= PARENB; //even parity
tty.c_cflag &= ~PARODD;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag |= (CLOCAL | CREAD);
tcsetattr(fd_mc, TCSANOW, &tty);
}
上面的代码是一个函数的片段,它初始化了两个串行端口(其中一个是微控制器的端口)。
编辑: 这里是流量控制的设置,有none.
tty.c_cflag &= ~CRTSCTS; //No hardware based flow control
tty.i_cflag &= ~(IXON | IXOFF | IXANY); //no software based flow control
在环形缓冲区和 pollin 的帮助下,从串行端口读取发生在线程内。该线程在主循环中被调用。代码如下:
void *thread_read()
{
struct sched_param param;
param.sched_priority = 97;
int ret_par = 0;
ret_par = sched_setscheduler(3, SCHED_FIFO, ¶m);
if (ret_par == -1) {
perror("sched_setscheduler");
return 0;
}
struct pollfd poll_fd[2];
int ret;
extern struct fifo mc_fifo, serial_fifo;
ssize_t t;
char c;
poll_fd[0].fd = fd_mc;
poll_fd[0].events = POLLIN;
poll_fd[1].fd = fd_serial;
poll_fd[1].events = POLLIN;
while(1) {
ret = poll(poll_fd, 2, 10000);
if(ret == -1) {
perror("poll");
}
if(poll_fd[0].revents & POLLIN) {
t = read(fd_mc, &c, 1);
if(t>0) {
fifo_in(&mc_fifo, c);
}
}
if(poll_fd[1].revents & POLLIN) {
t = read(fd_serial, &c, 1);
if(t>0) {
fifo_in(&serial_fifo, c);
}
}
}
pthread_exit(NULL);
}
在主循环内调用。
pthread_t read;
pthread_create(&read, NULL, thread_read, NULL);
写入缓冲区(fifo_in)的函数是
int fifo_in(struct fifo *f, char data)
{
if( (f->write + 1 == f->read) || (f->read == 0 && f->write + 1 == FIFO_SIZE) ) //checks if write one before read or at the end
{
printf("fifo in overflow\n");
return 0; //fifo full
}
else {
f->data[f->write] = data;
f->write++;
// printf("Byte in: Containing %4d\tData:\t%4d\n", BytesInReceiveBuffer(1), data); //Bytes contained in fifo of mc
if(f->write >= FIFO_SIZE) {
f->write = 0;
}
return 1; //success
}
}
这个函数基本上做的是检查读写位置在哪里,如果两个位置不重叠并且彼此相距超过+1
,则在缓冲区内写入data
.
当另一个函数需要环形缓冲区内的字节时,它会调用 GetByte
从环形缓冲区中读取字节。
获取字节
int GetByte(int port)
{
char c;
switch(port) {
case 0: //COM1
fifo_out(&serial_fifo,&c);
break;
case 1: //COM2
fifo_out(&mc_fifo,&c);
break;
}
return (int)c;
}
fifo_out
int fifo_out(struct fifo *f, char *data) {
if(f->read == f->write) {
printf("fifo in overfwrite\n");
*data = 0;
return 0;
}
else {
*data = f->data[f->read];
f->read++;
// printf("Byte out: Containing %4d\tData:\t%4d\n", BytesInReceiveBuffer(1), *data);
if(f->read >= FIFO_SIZE) {
f->read = 0;
}
return 1;
}
}
在 Linux 移植之前,DOS 版本中的一切都是顺序的。
吃了那一刻我最好的猜测是,read()
会减慢速度,在某些时候你会开始减慢缓冲区的读取速度,这再次阻止了线程的调用。也许我错了。目前我有点不知道这个错误到底是什么,甚至不知道如何解决这个问题。
感谢每一个好的建议。
您确定在应用程序中需要 fifo 吗?您的串行驱动程序很可能已经在内核中有相当大的缓冲区(通常是一页,通常为 4kB)。如果这就足够了,您可以通过 GetByte
对串行设备进行非阻塞读取来从根本上简化实现。
如果您想坚持这种设计,请考虑修改您的读取循环以一次读取多个字节。现在你需要为每个字节读取两个系统调用。
您一直在更改 PID 3 的调度 class。这可能不是您想要的。此外,这仅意味着一旦字节到达内核内部缓冲区(即 poll
returns 时),您的线程将 运行。如果通过 运行 在工作队列上执行作业将字节从硬件 FIFO 传输到缓冲区,并且该工作队列 运行s 在 SCHED_OTHER
中(大多数人这样做),则更改调度程序你的线程不会有预期的效果。经典的优先级倒置问题。您可能想要审核您的特定板使用的内核驱动程序。不过,如果您在每个 read
清空整个缓冲区,这应该不是什么问题。
如果此代码曾经在 SMP 系统上使用过,您很可能会想用锁来保护 read
和 write
指针,因为它们不是自动更新的。线程很难正确处理,您是否考虑过使用事件循环来代替?类似于 libev.