Linux-485收發切換延遲的解決方法
【前言】
- 本文引用各種資料甚多,而引用出處標明並不詳細,若有侵權,請聯絡刪除。
- 轉載請註明出處:https://www.cnblogs.com/leisure_chn/p/10381616.html
一、問題描述
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,那方向切換不及時將導致資料接收丟失。這就是問題所在。
二、解決方法
1. 解決思路
關於收發方向延遲問題,解決思路有如下幾種:
- 由硬體自動控制收發方向的切換,這種方式不需要軟體參與,硬體實現也很簡單,推薦使用
- 嘗試將作業系統HZ由預設的100改為1000,經測,tcdrain()延遲降為幾個ms,實際仍然不能滿足要求,而且比較影響系統性能
- 應用層控制方向切換,應用程式裡使用ioctl()方法,利用Linux串列埠驅動裡自帶的485功能。此方法需要全串口裡的RTS管腳作為方向腳。時間所限,此方法未研究明白
- 驅動層控制方向切換,修改串列埠驅動使支援485方向切換,此方法驗證可行
最後一種方法就是本文要描述的方法。
2. 知識儲備
解決此問題,需要有如下知識儲備:
- 瞭解485通訊原理
- 瞭解Linux終端裝置驅動架構,搞清楚板上串列埠對應的實際驅動原始檔
- 掌握Linux裝置驅動中的中斷處理機制:頂半部、底半部(tasklet、工作佇列、軟中斷)
3. 實現方法
本應用中對應的串列埠裝置驅動檔案為linux/drivers/tty/serial/8250/8250_core.c
3.1 由應用程式控制是否開啟串列埠裝置的485功能
在串列埠驅動裡切換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_portport; struct timer_listtimer;/* "no irq" timer */ struct list_headlist;/* ports on this IRQ */ unsigned shortcapabilities;/* port capabilities */ unsigned shortbugs;/* port bugs */ boolfifo_bug;/* min RX trigger if enabled */ unsigned inttx_loadsz;/* transmit fifo load size */ unsigned characr; unsigned charfcr; unsigned charier; unsigned charlcr; unsigned charmcr; unsigned charmcr_mask;/* mask of user bits */ unsigned charmcr_force;/* mask of forced bits */ unsigned charcur_iotype; /* Running I/O type */ unsigned intrpm_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 charlsr_saved_flags; #define MSR_SAVE_FLAGS UART_MSR_ANY_DELTA unsigned charmsr_saved_flags; struct uart_8250_dma*dma; struct serial_rs485rs485; /* 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()說明參上
3.2 在傳送過程的起始時刻拉高PIN_DIR
在串列埠傳送的起始時刻,即串列埠產生傳輸起始位的時刻,會呼叫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 ...... }
3.3 在傳送過程的結束時間拉低PIN_DIR
按照推理,以為在串列埠傳輸結束位的時候,會呼叫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收發方向切換非常準時,微秒級別的延遲,可以接受
3.4 幾種中斷底半部機制的對比
-
tasklet
tasklet執行於軟中斷上下文,執行時機通常是在頂半部返回的時候。tasklet處理函式中不可睡眠。 -
工作佇列
工作佇列執行於程序上下文(核心執行緒)。工作佇列處理函式中可以睡眠。 -
軟中斷(softirq)
tasklet是基於軟中斷(softirq)實現的。softirq通常在核心中使用,驅動程式不宜直接使用softirq。
總體來說,中斷優先順序高於軟中斷,軟中斷優先順序高於各執行緒。
在本例中,曾嘗試使用工作佇列,測得延遲仍有幾毫秒至十幾二十毫秒(記不清楚了),無法解決問題。
而使用tasklet則能將延遲控制得非常精確。從這一點也反映了程序上下文和軟中斷上下文的不同之處。
三、遺留問題
- tasklet處理函式中呼叫了自旋鎖,忙等判斷髮送結束時刻,作業系統將串列埠緩衝區資料全部扔給串列埠晶片到串列埠線上一包資料傳輸完成,這個過程存在一個時間段,在這個時間段內,處於忙等狀態,這會影響系統性能。優化方向是:研究是否能利用moderm的線控狀態,在傳輸線上資料傳輸完成的時刻,觸發一箇中斷,在此中斷處理中將485切換為接收狀態。
- 應用程式串列埠接收的read()函式一直處於阻塞狀態,直到資料在訊號線中傳輸完畢驅動層中有資料可讀。優化方向是:由驅動層接收在接收起始時刻和結束時刻分別嚮應用層發一個訊號,結束時刻定在串列埠接收超時中斷時刻,這樣應用程式可以獲知串列埠線何時處於接收忙碌狀態。這樣會使對485的支援機制更加完善,應用層有更多的控制空間。
四、參考資料
[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/