前言
最近做嵌入式Linux项目,有用到RS485。就顺带复习了一下串口、RS485的相关知识。
此篇文章主要是记录一下嵌入式Linux的RS485开发的基础知识和注意事项,与君共勉!
一、Linux串口基础
1.串口初始化
(1)在Linux一切皆为文件,串口也不例外,初始化串口之前,需要先打开设备,调用标准函数open:
int fd = open(RS485_ONE_UART_DEV, O_RDWR | O_NOCTTY);
O_RDWR 代表可读写,O_NOCTTY代表不成为端口的控制终端(否则任何输入都会中断程序的执行)
(2)获取串口参数:串口本身有参数,我们可以通过tcgetattr函数获得,并存入struct termios结构体
#include<termios.h>
struct termios options;
tcgetattr(fd, &options);
其中,struct termios结构体如下:
struct termios
{unsigned short c_iflag; /* 输入模式标志*/
unsigned short c_oflag; /* 输出模式标志*/
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /*区域模式标志或本地模式标志或局部模式*/
unsigned char c_line; /*行控制line discipline */
unsigned char c_cc[NCC]; /* 控制字符特性*/
};
(3)修改串口参数并保存:修改参数后,可调用tcsetattr函数保存
if((tcsetattr(fd,TCSANOW,&options))!=0)
{
perror("tcsetattr error");
return -2;
}
(4)串口参数介绍:
struct termios的c_iflag,表示输入模式,可用以下宏(支持或运算):
BRKINT:当在输入行中检测到一个终止状态时,产生一个中断。
TGNBRK:忽略输入行中的终止状态。
TCRNL:将接受到的回车符转换为新行符。
TGNCR:忽略接受到的新行符。
INLCR:将接受到的新行符转换为回车符。
IGNPAR:忽略奇偶校检错误的字符。
INPCK:对接收到的字符执行奇偶校检。
PARMRK:对奇偶校检错误作出标记。
ISTRIP:将所有接收的字符裁减为7比特。
IXOFF:对输入启用软件流控。
IXON:对输出启用软件流控。
如果BRKINT和TGNBRK标志都未被设置,则输入行中的终止状态就被读取为NULL(0X00)字符。
struct termios的c_oflag,表示输入模式,可用以下宏(支持或运算):
OPSOT:打开输出处理功能
ONLCR:将输出中的换行符转换为回车符
OCRNL:将回车符转换为换行符
ONOCR:第0行不输出回车符
ONLRET:不输出回车符
NLDLY:换行符延时选择
CRDLY:回车符延时
TABDLY:制表符延时
struct termios的c_cflag,表示控制模式,可用以下宏(支持或运算):
CLOCAL:忽略所有调制解调器的状态行
CREAD:启用字符接收器
CS5/CS6/CS7/CS8:发送或接收字符时使用5/6/7/8比特
CSTOPB:每个字符使用两停止位
HUPCL:关闭时挂断调制解调器
PARENB:启用奇偶校验码的生成和检测功能
PARODD:只使用奇检验而不用偶校验
struct termios的c_cflag,表示特殊控制字符:
c_cc[VMIN]:代表读数据的最小字节
c_cc[VTIME]:等待第一个字节的最大时间,单位100ms
其中:
MIN = 0, TIME = 0时:read立即返回,如果有待处理的字符,它们就会被返回,如果没有,read调用返回0,且不读取任何字符
MIN = 0, TIME > 0时:有字符处理或经过TIME个0.1秒后返回
MIN > 0, TIME = 0时:read一直等待,直到有MIN个字符可以读取,返回值是字符的数量.到达文件尾时返回0
MIN > 0, TIME > 0时:read调用时,它会等待接收一个字符.在接收到第一个字符及其后续的每个字符后,启用一个字符间隔定时器.当有MIN个字符可读或两字符间的时间间隔超进TIME个0.1秒时,read返回
(5)串口初始化例程:
/************************** struct define **************************/
/** uart param **/
typedef struct
{
int fd; //串口fd
int uart_id; //串口索引
int baudrate; //波特率
int databits; //数据位
char parity; //校验位
int stopbit; //停止位
}uart_param_t;
/************************************************************************************************************
/* brief: uart初始化函数,打开串口,并创建串口接收线程
* param: uart为串口参数
uart->baudrate波特率,选填2400/4800/9600/115200
uart->databits数据位,选填7/8
uart->parity校验位,选填'O'/'E'/'N'
uart->stopbit停止位,选填1/2
* return: 返回值为串口fd,小于0代表初始化串口设备失败
*/
static int app_uart_init(uart_param_t *uart)
{
int fd=-1;
int32_t ret = 0;
struct termios options;
printf("app_uart_init, uart_id:%d, baudrate:%d, databits:%d, parity:%d, stopbit:%d\n", uart->uart_id, uart->baudrate, uart->databits, uart->parity, uart->stopbit);
// 打开串口设备
if(uart->uart_id==UART_ONE_INDEX)
{
fd = open(RS485_ONE_UART_DEV, O_RDWR | O_NOCTTY);
}
else if(uart->uart_id==UART_TWO_INDEX)
{
fd = open(RS485_TWO_UART_DEV, O_RDWR | O_NOCTTY);
}
if (fd < 0)
{
perror("uart open failed\n");
return -1;
}
// 配置串口参数
tcgetattr(fd, &options);
options.c_iflag = IGNPAR;
options.c_oflag = 0;
options.c_lflag = 0;
options.c_cflag = (CLOCAL | CREAD); //本地连接、打开接收
switch( uart->baudrate )
{
case 2400:
options.c_cflag |= B2400;
break;
case 4800:
options.c_cflag |= B4800;
break;
case 9600:
options.c_cflag |= B9600;
break;
case 115200:
options.c_cflag |= B115200;
break;
default:
options.c_cflag |= B9600;
break;
}
switch( uart->databits )
{
case 7:
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
options.c_cflag |= CS8;
break;
}
switch( uart->parity )
{
case 'O':
options.c_cflag |= PARENB; //允许输出产生奇偶信息以及输入的奇偶校验(启用同位产生与侦测)
options.c_cflag |= PARODD; //输入和输出是奇校验(使用奇同位而非偶同位)
options.c_iflag |= INPCK; //启用输入奇偶检测
break;
case 'E':
options.c_cflag |= PARENB; //允许输出产生奇偶信息以及输入的奇偶校验(启用同位产生与侦测)
options.c_cflag &= ~PARODD; //输入和输出不是奇校验(使用偶同位而非奇同位)
options.c_iflag |= INPCK; //启用输入奇偶检测
break;
case 'N':
break;
}
if( uart->stopbit == 1 )
options.c_cflag &= ~CSTOPB; //设置一个停止位
else if ( uart->stopbit == 2 )
options.c_cflag |= CSTOPB; //设置两个停止位
options.c_cc[VMIN] = 0; /* 读数据时的最小字节数 */
options.c_cc[VTIME] = 50; /* 等待第1个数据的时间,单位0.1S:
* 如果VTIME*0.1秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
*/
tcflush(fd, TCIFLUSH);
if((tcsetattr(fd,TCSANOW,&options))!=0)
{
perror("tcsetattr error");
return -2;
}
return fd;
}
2.数据接收
串口的数据发送,调用标准的文件IO就可以了
int rLen = read(fd, rBuffer, len);
3.数据发送
串口的数据发送,也是调用标准的文件IO
int writed_len=write(fd, wbuffer, len);
(特别注意!write函数返回,只是代表数据写入到了串口"文件",并不意味着数据已经通过硬件发送出去了!
记住这一点,后面RS485也会涉及!)
二、RS485基础
1.RS485总线:
(1)RS485是半双工通讯,物理上由A、B两根总线组成。通过A、B线的电压差来表达0和1,从而传输串行数据
(2)由于数据的发送,需要同时调动A、B两根线的电压,因此总线上,同一时间,有且仅能有一个设备可以发送消息
(3)由于此特性,一般总线上只会有一个主机,负责主动发送消息;其他设备都为从机,只有收到属于自己的消息,才会发送消息(回复消息)
2.如何通过串口使用RS485:
(1)芯片的串口发送的是TTL电平数据,是全双工通讯,而RS485总线是半双工通讯;
(2)由于电平和通讯模式不同,要把TTL电平信号转换为RS485总线信号,需要使用RS485芯片
(3)RS485芯片有两种工作模式,分别是发送模式和接收模式,由一个使能IO控制,一般是高电平使能发送,低电平使能接收
(4)因此串口发送数据前,要通过使能IO,先使能发送;发送完后,使能接收
3.注意事项:
(1)RS485芯片,在接收和发送切换的时候,需要时间。因此切到换发送使能后,需要延时一小会,再调用write函数。具体的切换时间,根据芯片各有不同,拿到实物后实测一下比较稳妥。
(2)前面有提到,串口调用write函数返回,只是把数据写入到串口"文件";如果write函数返回,立马使能接收,会导致发送的数据大部分、甚至全部丢失!
(3)串口的波特率,指的是每秒能发送的bit数。而发送一个字节需要的bit数=起始位(1bit)+数据位(5~8bit)+校验位(1bit)+停止位(1/2bit)
(4)硬件发送串口数据的时间(单位ms) time_ms=数据字节数*(数据位+停止位+2)*1000/波特率
(5)因此write函数返回后,至少延时time_ms毫秒的时间,才能保证数据从硬件发送出去
三、嵌入式Linux的RS485收发例程
1.RS485发送
/* brief: uart发送数据
* param: fd:文件描述符
uart为串口参数
buffer包含待写入数据的缓冲区
len缓冲区字节数
* return: 大于等于0代表发送的字节数,小于0代表发送失败
*/
int app_uart_send(int fd, uart_param_t *uart, unsigned char *buffer, int32_t len)
{
printf("app_uart_send...\n");
if(fd<0)
{
perror("fd<0!\n");
return -1;
}
if((uart==NULL)||(buffer==NULL)||(len==0))
{
perror("send data err!\n");
return -2;
}
//使能脚置高电平
app_RS485_en_io_set_cmd(uart->uart_id,1);
//延时20ms(经验值),等待芯片进入发送模式
usleep(20*1000);
//发送数据到串口
int writed_len=write(fd, buffer, len);
//延时(理论时间+10)ms,等数据从硬件发送完成
int sec=len*(uart->databits+uart->stopbit+2)*1000/uart->baudrate+1; //硬件发送数据所需时间(向上取整),单位ms(适用于波特率2400~115200)
usleep((sec+10)*1000); //延时(理论时间+10)ms
//使能脚置低电平,使芯片进入接收模式
app_RS485_en_io_set_cmd(uart->uart_id,0);
if(writed_len != len)
{
perror("uart send data failed!\n");
}
return writed_len;
}
2.RS485接收
/* brief: uart接收数据
* param: fd:文件描述符
buffer用于存储读取数据的缓冲区
len缓冲区字节数
* return: 小于0代表参数错误,0代表没收到数据,大于0代表接收到的字节数
*/
int app_uart_receive(int fd, char *buffer, int32_t len)
{
if(fd<0)
{
perror("fd<0!\n");
return -1;
}
if((buffer==NULL)||(len==0))
{
perror("send data err!\n");
return -2;
}
uint8_t rBuffer[UART_RECV_BUF_SIZE]={0};
printf("app_uart_receive: read start \n");
int rLen = read(fd, rBuffer, UART_RECV_BUF_SIZE);
printf("app_uart_receive: end of read, rLen:%d \n", rLen);
if (rLen > 0)
{
printf("uart read total: %d\n", rLen);
//防止缓冲区过小导致异常
if(rLen>len)
{
memcpy(buffer,rBuffer,len);
}
else
{
memcpy(buffer,rBuffer,rLen);
}
}
return rLen;
}