RS-485(亦称TIA-485, EIA-485)作为一种半双工总线,其收发过程不能同时进行。 RS-485通信的具体硬件原理可查阅其他资料,此处不详述。本文仅描述其控制方法及相关问题。
通常由CPU引出三根管脚:两个UART管脚(记作PIN_RX、PIN_TX)和一个485收发方向控制管脚(记作PIN_DIR)。 这三根管脚会接在板上的485芯片上,485芯片再向板外引出“D+、D-”两根差分信号总线(差分信号具有搞干扰、传输距离远的优势)。
应用程序编写时,在原来的普通串口通信基础上,加上485收发方向控制即可。 具体说来,UART发送过程中,将PIN_DIR脚拉高,发送完毕再将PIN_DIR脚拉低,使485总线可以接收数据。 对于无操作系统的裸机程序来说,485通信非常简单。 但在Linux应用程序编写中,这个方向切换存在延迟问题。
Linux应用层485控制接口伪代码如下:
// 初始化串口
fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);
init_serial(fd, 9600, 8, 1, 'N');
set_485_dir(LOW); // 默认为接收状态
// 发送数据
set_485_dir(HIGH);
write(fd, buf, sizeof(buf));
tcdrain(fd); // 此句判断时刻不准,延时约10-20ms
set_485_dir(LOW);
// 接收数据
read();
经测试,set_485_dir()改变PIN_DIR脚的延迟很小,可忽略不计。tcdrain()却总是存在10-20ms的延迟。 tcdrain()是等待fd所引用的串口设备数据传输完毕。在物理上数据已传输完毕时,因为操作系统延迟原因,导致tcdrain()多停留了10-20ms,从而导致数据发送完后,PIN_DIR不能及时切换。 如果对接的485设备,接收和应答的延迟小于20ms,那方向切换不及时将导致数据接收丢失。这就是问题所在。
关于收发方向延迟问题,解决思路有如下几种:
最后一种方法就是本文要描述的方法。
解决此问题,需要有如下知识储备:
本应用中对应的串口设备驱动文件为linux/drivers/tty/serial/8250/8250_core.c
在串口驱动里切换485方向对性能有一些影响。 而某些应用可能只需要标准串口,不需要支持485模式。 因此最好由应用程序来控制,是使用标准串口还是支持485模式的串口。 这主要利用ioctl()实现。
应用程序在初始化打开串口时,禁用/使能串口的485模式
fd = open(...);
init_serial(fd, ...);
struct serial_rs485 rs485conf;
rs485conf.flags |= SER_RS485_ENABLED; // 使能本串口485模式,默认禁用
ioctl(fd, TIOCSRS485, &rs485conf);
驱动程序中对使能了485模式的串口作特殊处理。 利用struct uart_8250_port结构体中的struct serial_rs485 rs485成员判断串口是否支持485模式。 在serial_8250.h中有定义rs485数据成员,以及设置此数据成员的成员函数rs485_config
// noted by xx@xx: in serial_8250.h
/*
* This should be used by drivers which want to register
* their own 8250 ports without registering their own
* platform device. Using these will make your driver
* dependent on the 8250 driver.
*/
struct uart_8250_port {
struct uart_port port;
struct timer_list timer; /* "no irq" timer */
struct list_head list; /* ports on this IRQ */
unsigned short capabilities; /* port capabilities */
unsigned short bugs; /* port bugs */
bool fifo_bug; /* min RX trigger if enabled */
unsigned int tx_loadsz; /* transmit fifo load size */
unsigned char acr;
unsigned char fcr;
unsigned char ier;
unsigned char lcr;
unsigned char mcr;
unsigned char mcr_mask; /* mask of user bits */
unsigned char mcr_force; /* mask of forced bits */
unsigned char cur_iotype; /* Running I/O type */
unsigned int rpm_tx_active;
/*
* Some bits in registers are cleared on a read, so they must
* be saved whenever the register is read but the bits will not
* be immediately processed.
*/
#define LSR_SAVE_FLAGS UART_LSR_BRK_ERROR_BITS
unsigned char lsr_saved_flags;
#define MSR_SAVE_FLAGS UART_MSR_ANY_DELTA
unsigned char msr_saved_flags;
struct uart_8250_dma *dma;
struct serial_rs485 rs485;
/* 8250 specific callbacks */
int (*dl_read)(struct uart_8250_port *);
void (*dl_write)(struct uart_8250_port *, int);
***int (*rs485_config)(struct uart_8250_port *,
struct serial_rs485 *rs485);***
};
但serial_8250.c中默认并未实现rs485_config函数,那我们自己实现,如下: 1) 驱动层编写485配置函数
// add by xx@xx begin
static int serial8250_rs485_config(struct uart_8250_port *up,
struct serial_rs485 *rs485)
{
if (rs485->flags & SER_RS485_ENABLED) {
printk(KERN_INFO "uart %d set 485 on\n", up->port.line);
gpio_485_set_direction(true);
gpio_485_set_value(false);
tasklet_init(&s485_tasklet, serial8250_485_do_tasklet, (unsigned long)&up->port);
}
else {
printk(KERN_INFO "uart %d set 485 off\n", up->port.line);
}
memcpy(&up->rs485, rs485, sizeof(*rs485));
return 0;
}
// add by xx@xx end
此函数在应用层调用ioctl()函数时,会被驱动层调用执行,此函数作了两件事:
a. 将第二个参数rs485保存在第一个参数up里,第一个参数关联具体的某个串口设备(关联应用层里的ioctl(fd)中的fd)
b. 判断参数是否使能了485模式,若使能了,则将485方向设置为接收,并注册中断底半部tasklet处理函数serial8250_485_do_tasklet
2) 驱动层注册485配置函数
int serial8250_register_8250_port(struct uart_8250_port *up)
{
struct uart_8250_port *uart;
int ret = -ENOSPC;
if (up->port.uartclk == 0)
return -EINVAL;
mutex_lock(&serial_mutex);
// add by xx@xx begin
memset((void *)&up->rs485, 0, sizeof(up->rs485));
up->rs485_config = serial8250_rs485_config;
// add by xx@xx end
......
}
3)应用层open()打开串口时,驱动层调用链
serial8250_probe()->
serial8250_register_8250_port()->
up->rs485_config = serial8250_rs485_config;
4) 应用层ioctl()使能串口485模式时,ioctl()在驱动底层的调用代码
// 下列代码为系统自带代码,无任何改动
static int serial8250_ioctl(struct uart_port *port, unsigned int cmd,
unsigned long arg)
{
struct uart_8250_port *up =
container_of(port, struct uart_8250_port, port);
int ret;
struct serial_rs485 rs485_config;
if (!up->rs485_config)
return -ENOIOCTLCMD;
switch (cmd) {
case TIOCSRS485: // 设置
if (copy_from_user(&rs485_config, (void __user *)arg,
sizeof(rs485_config)))
return -EFAULT;
ret = up->rs485_config(up, &rs485_config);
if (ret)
return ret;
memcpy(&up->rs485, &rs485_config, sizeof(rs485_config));
return 0;
case TIOCGRS485: // 获取
if (copy_to_user((void __user *)arg, &up->rs485,
sizeof(up->rs485)))
return -EFAULT;
return 0;
default:
break;
}
return -ENOIOCTLCMD;
}
调用链:
serial8250_ioctl()->
up->rs485_config(up, &rs485_config)->
serial8250_rs485_config() // 自己实现的函数
serial8250_rs485_config()说明参上
在串口发送的起始时刻,即串口产生传输起始位的时刻,会调用serial8250_start_tx(),在此函数中将PIN_DIR拉高
static void serial8250_start_tx(struct uart_port *port)
{
struct uart_8250_port *up = up_to_u8250p(port);
// add by xx@xx begin
if (up->rs485.flags & SER_RS485_ENABLED) {
gpio_485_set_value(true);
}
// add by xx@xx end
......
}
按照推理,以为在串口传输结束位的时候,会调用serial8250_stop_tx(),那在此函数中将PIN_DIR拉低,任务就完成了。 但是,加打印发现,实际此函数从未被调用。 缕一下代码,找到串口发送的结束时刻:8250串口的收发数据是通过中断方式实现的,串口的结束时刻在中断处理程序中判断, 1) 中断处理函数的注册
serial8250_init()->
serial8250_isa_init_ports()->
set_io_from_upio()->
p->handle_irq = serial8250_default_handle_irq;
2) 中断处理函数的调用
serial8250_default_handle_irq()->
serial8250_handle_irq()->
serial8250_tx_chars()->
3) 找到位置了,就在serial8250_tx_chars()中调用底半部机制tasklet
void serial8250_tx_chars(struct uart_8250_port *up)
{
struct uart_port *port = &up->port;
struct circ_buf *xmit = &port->state->xmit;
int count;
if (port->x_char) {
serial_out(up, UART_TX, port->x_char);
port->icount.tx++;
port->x_char = 0;
return;
}
if (uart_tx_stopped(port)) {
serial8250_stop_tx(port);
return;
}
if (uart_circ_empty(xmit)) {
__stop_tx(up);
return;
}
count = up->tx_loadsz;
do {
serial_out(up, UART_TX, xmit->buf[xmit->tail]);
xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
port->icount.tx++;
if (uart_circ_empty(xmit))
break;
if (up->capabilities & UART_CAP_HFIFO) {
if ((serial_port_in(port, UART_LSR) & BOTH_EMPTY) !=
BOTH_EMPTY)
break;
}
} while (--count > 0);
if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
uart_write_wakeup(port);
DEBUG_INTR("THRE...");
/*
* With RPM enabled, we have to wait once the FIFO is empty before the
* HW can go idle. So we get here once again with empty FIFO and disable
* the interrupt and RPM in __stop_tx()
*/
if (uart_circ_empty(xmit) && !(up->capabilities & UART_CAP_RPM))
{
__stop_tx(up);
// add by xx@xx begin
if (up->rs485.flags & SER_RS485_ENABLED)
tasklet_hi_schedule(&s485_tasklet);
// add by xx@xx end
}
}
注:tasklet_hi_schedule()和tasklet_schedule()的区别:
void tasklet_schedule(struct tasklet_struct *t); 调度tasklet执行,如果tasklet在运行中被调度,它在完成后会再次运行;这保证了在其他事件被处理当中发生的事件受到应有的注意。这个做法也允许一个tasklet重新调度它自己。
void tasklet_hi_schedule(struct tasklet_struct *t); 和tasklet_schedule()类似,只是在更高优先级执行。当软中断处理运行时, 将在其他软中断之前tasklet_hi_schedule(),只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期。
void tasklet_hi_schedule_first(struct tasklet_struct *t); 此函数的主要作用是将参数t代表的软中断添加到向量tasklet_hi_vec的头部,并触发一个软中断。而tasklet_hi_schedule()则是将参数t代表的软中断 添加到向量tasklet_hi_vec的尾部,因此tasklet_hi_schedule_first()添加的tasklet比tasklet_hi_schedule()的优先级更高。
tasklet_schedule使用TASKLET_SOFTIRQ软中断索引号,tasklet_hi_schedule和tasklet_hi_schedule_first()使用HI_SOFTIRQ软中断索引号。 在Linux支持的多种软中断中,HI_SOFTIRQ具有最高的优先级。
4) tasklet处理函数的实现
// TODO: custom a new macro to avoid warnings
#define my_container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member)))
static struct tasklet_struct s485_tasklet;
void serial8250_485_do_tasklet(unsigned long);
void serial8250_485_do_tasklet(unsigned long param)
{
struct uart_port *port;
struct uart_state *state;
struct tty_struct *tty;
struct ktermios termios;
unsigned int baud;
int bit_width;
port = (struct uart_port *)param;
#if 0
struct circ_buf *xmit = &port->state->xmit;
unsigned long flags;
unsigned int lsr;
while (1)
{
spin_lock_irqsave(&port->lock, flags);
lsr = serial_port_in(port, UART_LSR);
spin_unlock_irqrestore(&port->lock, flags);
if (uart_circ_empty(xmit) && ((lsr & BOTH_EMPTY) == BOTH_EMPTY))
{
break;
}
}
#else
while (port->ops->tx_empty(port) != TIOCSER_TEMT)
{
;
}
#endif
state = my_container_of(port, struct uart_state, uart_port);
tty = my_container_of(state, struct tty_struct, driver_data);
termios = tty->termios;
baud = uart_get_baud_rate(port, &termios, NULL, 1200, 115200);
bit_width = (baud > 0) ? 1000000/baud : 0;
bit_width = (bit_width > 50) ? (bit_width-50) : 0; // Measured delay value is 50 us
udelay(bit_width); // a stop bit
gpio_485_set_value(false);
}
注意:上述代码中udelay(bit_width)是为了延迟一个stop bit的时间 用示波器测一下,485收发方向切换非常准时,微秒级别的延迟,可以接受
总体来说,中断优先级高于软中断,软中断优先级高于各线程。
在本例中,曾尝试使用工作队列,测得延迟仍有几毫秒至十几二十毫秒(记不清楚了),无法解决问题。 而使用tasklet则能将延迟控制得非常精确。从这一点也反映了进程上下文和软中断上下文的不同之处。
[1] https://zh.wikipedia.org/wiki/EIA-485 [2] https://blog.csdn.net/u012351051/article/details/69223326 [3] http://kuafu80.blog.163.com/blog/static/122647180201431625820150/ [4] http://blog.chinaunix.net/uid-20768928-id-5077401.html [5] https://blog.csdn.net/u013304850/article/details/77165265 [6] http://guojing.me/linux-kernel-architecture/posts/soft-irq/ [7] https://blog.csdn.net/ezimu/article/details/54851148