1. 总述
我们使用C语言编写多线程程序来实现RS485的发送和接收。由于RS485是半双工通信,同一时刻只能发送或接收,因此我们需要一个互斥锁(mutex)来控制对串口的访问,避免同时发送和接收。
一般步骤:
1)包含必要的头文件;
2)定义互斥锁,用于保护串口资源;
3)创建发送线程和接收线程;
4)发送线程函数:获取互斥锁,发送数据,释放互斥锁;
5)接收线程函数:获取互斥锁,接收数据,释放互斥锁;
6)主函数:初始化互斥锁,创建线程,等待线程结束。
注意:实际串口操作需要用到串口设备文件(如"/dev/ttyS0")和相应的串口配置,这里我们使用伪代码表示串口操作。
2. 具体方案
(1)初步方案
- 方案1
由于RS485通常需要控制方向(发送使能),这里假设我们使用一个GPIO来控制方向。但在多线程中,我们通过互斥锁保证同一时间只有一个操作(发送或接收),并且在发送前设置方向为发送,发送完后立即设置为接收。同样,接收线程在接收前也应确保方向为接收(但通常我们在发送时切换方向,接收时切换回来)。
然而,为了简化,我们可以在发送线程中设置方向,接收线程则一直处于接收状态(除了发送期间)。但注意:在发送期间,接收是无效的,所以接收线程在发送期间应该等待。因此,我们使用互斥锁来保护整个发送和接收过程,确保发送时不会接收,接收时也不会发送(但接收线程在等待锁的时候,实际上是在等待发送完成)。
- 方案2
另一种做法:由于半双工,可以将发送和接收都放在同一个互斥锁的保护下,这样就不会同时进行。但是这样会导致接收线程在等待锁时无法接收数据,可能会丢失数据。因此,需要更精细的控制。
- 小结
实际中,RS485通信通常由发送方控制总线,发送完成后释放总线,接收方在总线上没有数据时进行接收。因此,我们设计:
- 发送线程:在需要发送时,获取锁,设置方向为发送,发送数据,然后设置方向为接收,释放锁。
- 接收线程:尝试获取锁,但如果获取不到(说明正在发送),则等待;获取到锁后,进行接收操作,然后释放锁。
但是这样接收线程在发送时会被阻塞,而发送完成后接收线程才能接收。这并不符合实际,因为发送时接收线程阻塞,但发送完成后,接收线程才能开始接收,而此时可能已经错过了数据。
(2)改进措施
因此,我们需要重新考虑:实际上,接收线程应该一直处于接收状态,除非发送线程正在发送。所以,我们可以这样:
- 使用一个条件变量,当发送线程工作时,接收线程等待;发送完成后,通知接收线程开始接收。
然而,这仍然有问题,因为接收是持续的过程。
所以,另一种做法是:
- 发送线程在发送时,需要控制方向,并且在此期间接收线程不能操作串口(因为方向改变了,接收会出错)。
所以接收线程在发送期间应该停止接收,但这样可能会丢失数据。
因此通常的做法是:
- 在发送线程中,发送前设置方向为发送,然后发送数据,发送完后立即设置方向为接收;
- 而接收线程则一直尝试读取串口,但在发送期间,由于方向是发送,接收线程读到的可能是发送出去的数据(自发自收)或者乱码。所以,我们需要在软件层面过滤掉发送期间的数据。
考虑到复杂性,我们这里采用简单的互斥锁保护整个串口操作(包括设置方向和读写),保证同一时间只有一个线程操作串口。这样,接收线程在发送期间被阻塞,不会读取数据。但是,这样会导致发送期间接收线程不能接收,可能会丢失其它设备发送过来的数据(如果发送时间较长)。因此,在实际应用中,发送应该尽量快速,并且接收线程的阻塞时间要尽可能短。
另外,也可以考虑将接收线程设计为中断驱动(使用信号驱动I/O或异步I/O),但题目要求用线程。因此,我们采用互斥锁保护串口操作,包括设置方向。
因此,我们采用互斥锁保护串口操作,包括设置方向。
假设有一个控制方向的函数:set_direction(int direction); // 0:接收,1:发送
伪代码如下:
- 发送线程
while (1)
{
pthread_mutex_lock(&mutex);
set_direction(1); //设置为发送
write(serial_fd, send_buf, send_len);
set_direction(0); //设置为接收
pthread_mutex_unlock(&mutex); //其它操作,比如延时等
}
- 接收线程
while (1)
{
pthread_mutex_lock(&mutex);
read(serial_fd, recv_buf, recv_buf_size); //这里会阻塞直到有数据,但锁会导致发送线程无法发送 // 但是这样,如果接收线程一直阻塞在read上,那么发送线程就无法获取锁,造成死锁。
pthread_mutex_unlock(&mutex);
}
因此,需要使用非阻塞读取。或者设置超时;或者使用多路复用(如select)来等待数据,并且每次只读取一次;再或者,我们可以将接收设计为:每次只读取一个字节,然后立即释放锁,但这样效率低,且可能被发送线程频繁打断。
更好的做法:使用条件变量,或者将串口设置为非阻塞,然后使用select等待,在等待前释放锁,但这样比较复杂。
为了避免接收线程长时间占用锁,可以使用非阻塞I/O,并在循环中尝试读取,每次读取一定数量后释放锁。
但是,这样可能会增加复杂性。因此,我们采用另一种方法:在接收线程中,使用select或poll等待串口可读,并且设置一个超时时间(比如100ms),这样在等待期间,锁是释放的,发送线程可以获取锁进行发送。当超时后,接收线程再次获取锁,然后读取数据(如果有数据的话)。
然而,这样并不能完全避免数据丢失,因为从检测到可读到实际读取之间,如果发送线程获取了锁,那么接收线程会等待,而数据可能已经被接收线程读取到缓冲区,但还没有被应用层读取。
考虑到实时性要求,我们可能需要更高级的机制。但为了简化,我们假设发送是周期性的,并且发送时间间隔足够大,接收线程有足够时间读取。
由于问题复杂,我们简化:假设硬件支持自动方向控制(例如有的RS485芯片支持自动切换),这样我们就不需要软件控制方向了。那么,可以独立进行发送和接收,不需要互斥锁。但题目没有说明,所以我们还是按照软件控制方向。
考虑到这些复杂性,我们决定使用一个专门的线程来管理方向,而发送和接收通过队列进行。但这样会增加复杂性。
因此,我们采用一个折中方案:将发送和接收放在同一个线程中,但题目要求两个线程,所以不符合。
重新考虑:将方向控制放在发送线程中,并且接收线程在读取时不需要改变方向,但是接收线程在读取时,发送线程不能发送(因为方向可能会被改变)。所以,我们仍然需要互斥锁。
最终方案:
接收线程使用非阻塞读取,并且每次读取前检查是否有数据,如果没有数据,则释放锁,让出发送线程。
具体步骤的伪代码如下:
- 接收线程
while (1)
{
pthread_mutex_lock(&mutex);
int n = read(serial_fd, recv_buf, recv_buf_size); //非阻塞读取
if (n > 0)
{
//处理数据
}
else if (n == 0)
{
//没有数据,或者对方关闭?这里忽略
}
else
{
// 错误或EAGAIN
}
pthread_mutex_unlock(&mutex);
usleep(10000); //休眠10ms,避免频繁占用锁
}
- 发送线程
while (1)
{
//等待发送条件满足(比如有数据要发送)
pthread_mutex_lock(&mutex);
set_direction(1);
write(serial_fd, send_buf, send_len); //注意:write后需要等待数据发送完成,可以调用tcdrain(serial_fd)来等待发送完成,然后才能切换方向
tcdrain(serial_fd); //等待发送完成
set_direction(0);
pthread_mutex_unlock(&mutex);
//发送间隔
sleep(1);
}
此方案的不足是:
接收线程在发送线程发送期间会休眠,而发送线程发送时,接收线程无法获取锁,所以不会读到发送期间的数据。同时,发送线程发送完成后,接收线程才能读取。
具体细节如下:
如果接收线程在发送线程开始发送前已经获取了锁,那么发送线程就会等待接收线程释放锁,而接收线程在读取时(非阻塞读取,如果没有数据,会立即返回)然后释放锁,这样发送线程就可以发送了。
但是,在发送线程发送期间,接收线程再次尝试获取锁,就会阻塞,直到发送完成。这样,在发送期间,接收线程不会读取数据,从而避免了方向问题。
然而,在发送完成后,接收线程读取数据,此时总线上可能有其它设备发送过来的数据,所以可以正常接收。
这样接收线程在无数据时会频繁占用锁,导致发送线程无法及时发送。因此,我们可以使用条件变量来协调:当没有数据要发送时,接收线程可以长时间占用锁(即等待数据),当有数据要发送时,发送线程通知接收线程释放锁。但这样更复杂。
最终,我们采用一个更为简单的超时机制:接收线程每次占用锁的时间很短(非阻塞读取,然后立即释放),然后休眠一段时间。这样,发送线程可以很快获取锁。
具体的框架、步骤及代码示例见下一回。
3万+

被折叠的 条评论
为什么被折叠?



