Linux 串列埠程式設計 串列埠裝置程式開發

SoldierJazz2019發表於2017-05-19

Linux 串列埠程式設計和程式相對來說是很簡單的,之所以用部落格連載來展示,主要是想在學會使用的基礎上掌握相關背景,原理以及注意事項。相信在遇到問題的時候,我們就不會對於技術的概念和 API 的使用淺嘗輒止了。下面進入具體應用案例,由於現在很多電腦已經沒有引出串列埠以及波特率範圍會受到限制,這裡我以 CH340 USB 轉串列埠晶片製作的模組為基礎講解串列埠應用程式開發,關於該晶片在 Linux 系統的使用以及驅動載入可以參考:CH340 Linux驅動使用教程

裝置的開啟與關閉

1. int libtty_open(const char *devname);

函式功能:根據傳入的串列埠裝置名開啟相應的裝置。成功返回裝置控制程式碼,失敗返回-1。

2. int libtty_close(int fd);

函式功能:關閉開啟的裝置控制程式碼。成功返回0,失敗返回負值。

裝置的配置

1. int libtty_setopt(int fd, int speed, char databits, char stopbits, char parity);

函式功能:配置串列埠裝置,傳入引數依次為波特率設定、資料位設定、停止位設定、檢驗設定。

Notes:裝置開啟前,可以通過 ls /dev 確認自己的硬體裝置名,對於 USB 轉串列埠 IC,在系統下名稱為 "ttyUSBx",裝置序號是根據插入主機的先後順序自動分配的,這裡我的為 "ttyUSB0",讀者根據自己的需要修改。

/**
 * libtty_open - open tty device
 * @devname: the device name to open
 *
 * In this demo device is opened blocked, you could modify it at will.
 */
int libtty_open(const char *devname)
{
	int fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY); 
	int flags = 0;
	
	if (fd == -1) {                        
		perror("open device failed");
		return -1;            
	}
	
	flags = fcntl(fd, F_GETFL, 0);
	flags &= ~O_NONBLOCK;
	if (fcntl(fd, F_SETFL, flags) < 0) {
		printf("fcntl failed.\n");
		return -1;
	}
		
	if (isatty(fd) == 0)
	{
		printf("not tty device.\n");
		return -1;
	}
	else
		printf("tty device test ok.\n");
	
	return fd;
}

Note:
  • 傳入的 devname 引數為裝置絕對路徑;
  • O_NOCTTY標誌用於通知系統,這個程式不會成為對應這個裝置的控制終端。如果沒有指定這個標誌,那麼任何一個輸入(如SIGINT等)都將會影響使用者的程式;
  • O_NDELAY標誌與O_NONBLOCK 等效,但這裡不僅僅是設定為非阻塞,還用於通知系統,這個程式不關心 DCD 訊號線所處的狀態(即與裝置相連的另一端是否啟用或者停止)。如果使用者指定了這一標誌,則程式將會一直處在休眠狀態,直到 DCD 訊號線被啟用;
  • 使用 fcntl 函式恢復裝置狀態為阻塞狀態,在資料收發時就會進行等待;
  • 使用 isatty函式測試當前開啟的裝置控制程式碼是否關聯到終端裝置,如果不是 tty 裝置,那麼也返回出錯;
/**
 * libtty_setopt - config tty device
 * @fd: device handle
 * @speed: baud rate to set
 * @databits: data bits to set
 * @stopbits: stop bits to set
 * @parity: parity set
 *
 * The function return 0 if success, or -1 if fail.
 */
int libtty_setopt(int fd, int speed, int databits, int stopbits, char parity)
{
	struct termios newtio;
	struct termios oldtio;
	int i;
	
	bzero(&newtio, sizeof(newtio));
	bzero(&oldtio, sizeof(oldtio));
	
	if (tcgetattr(fd, &oldtio) != 0) {
		perror("tcgetattr");    
		return -1; 
	}
	newtio.c_cflag |= CLOCAL | CREAD;
	newtio.c_cflag &= ~CSIZE;
 
	/* set tty speed */
	for (i = 0; i < sizeof(speed_arr) / sizeof(int); i++) {
		if (speed == name_arr[i]) {      
			cfsetispeed(&newtio, speed_arr[i]); 
			cfsetospeed(&newtio, speed_arr[i]);   
		} 
	}
	
	/* set data bits */
	switch (databits) {
	case 5:                
		newtio.c_cflag |= CS5;
		break;
	case 6:                
		newtio.c_cflag |= CS6;
		break;
	case 7:                
		newtio.c_cflag |= CS7;
		break;
	case 8:    
		newtio.c_cflag |= CS8;
		break;  
	default:   
		fprintf(stderr, "unsupported data size\n");
		return -1; 
	}
	
	/* set parity */
	switch (parity) {  
	case 'n':
	case 'N':
		newtio.c_cflag &= ~PARENB;    /* Clear parity enable */
		newtio.c_iflag &= ~INPCK;     /* Disable input parity check */
		break; 
	case 'o':  
	case 'O':    
		newtio.c_cflag |= (PARODD | PARENB); /* Odd parity instead of even */
		newtio.c_iflag |= INPCK;     /* Enable input parity check */
		break; 
	case 'e': 
	case 'E':  
		newtio.c_cflag |= PARENB;    /* Enable parity */   
		newtio.c_cflag &= ~PARODD;   /* Even parity instead of odd */  
		newtio.c_iflag |= INPCK;     /* Enable input parity check */
		break;
	default:  
		fprintf(stderr, "unsupported parity\n");
		return -1; 
	} 
	
	/* set stop bits */ 
	switch (stopbits) {  
	case 1:   
		newtio.c_cflag &= ~CSTOPB; 
		break;
	case 2:   
		newtio.c_cflag |= CSTOPB; 
		break;
	default:   
		perror("unsupported stop bits\n"); 
		return -1;
	}

	newtio.c_cc[VTIME] = 0;	  /* Time-out value (tenths of a second) [!ICANON]. */
	newtio.c_cc[VMIN] = 0;    /* Minimum number of bytes read at once [!ICANON]. */
	
	tcflush(fd, TCIOFLUSH);  
	
	if (tcsetattr(fd, TCSANOW, &newtio) != 0)  
	{
		perror("tcsetattr");
		return -1;
	}
	return 0;
}
Note:
  • 首先儲存了原先串列埠配置,為了方便演示,這裡儲存為區域性變數,實際使用時是需要把配置儲存到全域性 termios 結構體中的。使用tcgetattr還可以測試配置是否正確、串列埠是否可用等。返回值參見 man 手冊;
  • 使用 CLOCAL 用於忽略所有 MODEM 狀態訊號線,CREAD 標誌用於使能接收。CSIZE 為資料位掩碼;
  • 呼叫 cfsetispeedcfsetospeed引數設定波特率,函式中引用了兩個陣列,在後面的完整程式碼中會看到,這樣書寫可以簡化設定程式碼;
  • 通過控制 c_cflag 與 c_iflag 配置串列埠資料位、停止位以及校驗設定等;
  • VTIMEVMIN作用已經講解多次,不再贅述,值得注意的是,TIME 值的單位是十分之一秒
  • 通過 tcflush清空輸入和輸出緩衝區,根據實際需要修改;
  • 最後通過 tcsetattr 函式對將配置實際作用於串列埠;
資料讀寫直接使用 readwrite 函式介面就可以了,因此沒有列舉出來。下面給出完整的串列埠讀寫測試程式碼:
/* TTY testing utility (using tty driver)
 * Copyright (c) 2017
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License.
 *
 * Cross-compile with cross-gcc -I /path/to/cross-kernel/include
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>  
#include <termios.h>  
#include <errno.h>   
#include <string.h>
#include <sys/types.h> 
#include <sys/stat.h>  

int speed_arr[] = {
	B115200,
	B57600,
	B38400,
	B19200,
	B9600,
	B4800,
	B2400,
	B1200,
	B300
};

int name_arr[] = {
	115200,
	57600,
	38400,
	19200,
	9600,
	4800,
	2400,
	1200,
	300
};

/**
 * libtty_setopt - config tty device
 * @fd: device handle
 * @speed: baud rate to set
 * @databits: data bits to set
 * @stopbits: stop bits to set
 * @parity: parity set
 *
 * The function return 0 if success, or -1 if fail.
 */
int libtty_setopt(int fd, int speed, int databits, int stopbits, char parity)
{
	struct termios newtio;
	struct termios oldtio;
	int i;
	
	bzero(&newtio, sizeof(newtio));
	bzero(&oldtio, sizeof(oldtio));
	
	if (tcgetattr(fd, &oldtio) != 0) {
		perror("tcgetattr");    
		return -1; 
	}
	newtio.c_cflag |= CLOCAL | CREAD;
	newtio.c_cflag &= ~CSIZE;
 
	/* set tty speed */
	for (i = 0; i < sizeof(speed_arr) / sizeof(int); i++) {
		if (speed == name_arr[i]) {      
			cfsetispeed(&newtio, speed_arr[i]); 
			cfsetospeed(&newtio, speed_arr[i]);   
		} 
	}
	
	/* set data bits */
	switch (databits) {
	case 5:                
		newtio.c_cflag |= CS5;
		break;
	case 6:                
		newtio.c_cflag |= CS6;
		break;
	case 7:                
		newtio.c_cflag |= CS7;
		break;
	case 8:    
		newtio.c_cflag |= CS8;
		break;  
	default:   
		fprintf(stderr, "unsupported data size\n");
		return -1; 
	}
	
	/* set parity */
	switch (parity) {  
	case 'n':
	case 'N':
		newtio.c_cflag &= ~PARENB;    /* Clear parity enable */
		newtio.c_iflag &= ~INPCK;     /* Disable input parity check */
		break; 
	case 'o':  
	case 'O':    
		newtio.c_cflag |= (PARODD | PARENB); /* Odd parity instead of even */
		newtio.c_iflag |= INPCK;     /* Enable input parity check */
		break; 
	case 'e': 
	case 'E':  
		newtio.c_cflag |= PARENB;    /* Enable parity */   
		newtio.c_cflag &= ~PARODD;   /* Even parity instead of odd */  
		newtio.c_iflag |= INPCK;     /* Enable input parity check */
		break;
	default:  
		fprintf(stderr, "unsupported parity\n");
		return -1; 
	} 
	
	/* set stop bits */ 
	switch (stopbits) {  
	case 1:   
		newtio.c_cflag &= ~CSTOPB; 
		break;
	case 2:   
		newtio.c_cflag |= CSTOPB; 
		break;
	default:   
		perror("unsupported stop bits\n"); 
		return -1;
	}

	newtio.c_cc[VTIME] = 0;	  /* Time-out value (tenths of a second) [!ICANON]. */
	newtio.c_cc[VMIN] = 0;    /* Minimum number of bytes read at once [!ICANON]. */
	
	tcflush(fd, TCIOFLUSH);  
	
	if (tcsetattr(fd, TCSANOW, &newtio) != 0)  
	{
		perror("tcsetattr");
		return -1;
	}
	return 0;
}

/**
 * libtty_open - open tty device
 * @devname: the device name to open
 *
 * In this demo device is opened blocked, you could modify it at will.
 */
int libtty_open(const char *devname)
{
	int fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY); 
	int flags = 0;
	
	if (fd == -1) {                        
		perror("open device failed");
		return -1;            
	}
	
	flags = fcntl(fd, F_GETFL, 0);
	flags &= ~O_NONBLOCK;
	if (fcntl(fd, F_SETFL, flags) < 0) {
		printf("fcntl failed.\n");
		return -1;
	}
		
	if (isatty(fd) == 0)
	{
		printf("not tty device.\n");
		return -1;
	}
	else
		printf("tty device test ok.\n");
	
	return fd;
}

/**
 * libtty_close - close tty device
 * @fd: the device handle
 *
 */
int libtty_close(int fd)
{
	return close(fd);
}

void tty_test(int fd)
{
	int nwrite, nread;
	char buf[100];
	
	memset(buf, 0x32, sizeof(buf));
	
	while (1) {
		nwrite = write(fd, buf, sizeof(buf));
		printf("wrote %d bytes already.\n", nwrite);
		nread = read(fd, buf, sizeof(buf));
		printf("read %d bytes already.\n", nread);
		sleep(2);
	}
	
}

int main(int argc, char *argv[])
{
	int fd;
	int ret;
	
	fd = libtty_open("/dev/ttyUSB0");
	if (fd < 0) {
		printf("libtty_open error.\n");
		exit(0);
	}
	
	ret = libtty_setopt(fd, 9600, 8, 1, 'n');
	if (ret != 0) {
		printf("libtty_setopt error.\n");
		exit(0);
	}

	tty_test(fd);
	
	ret = libtty_close(fd);
	if (ret != 0) {
		printf("libtty_close error.\n");
		exit(0);
	}
}
執行成功的話,會在終端螢幕上看到每隔兩秒輸出串列埠成功傳送和接收的位元組數,測試時可以直接短接串列埠的傳送和接收引腳進行測試。以下為成功測試截圖:



有疑問的讀者可以給我郵件或者評論,覺得對你有幫助就點贊吧~:-D


相關文章