赞
踩
最近被安排实现 linux rs485 串口通信。期间遇到各种问题,现在加以分析总结。
提到全双工,就不能不提与之密切对应的另一个概念,那就是“半双工(Half Duplex)”,
所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子,一条窄窄的马路,同时只能有一辆车通过,
当目前有两量车对开,这种情况下就只能一辆先过,等到头儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台。
上图为SP3485原理图
Pin1 - RO: 接收器输出
Pin2 - RE#:接收器输出使能 (低电平有效)
Pin3 - DE: 驱动器输出使能 (高电平有效)
Pin4 - DI: 驱动器输入
Pin5 - GND: 连接地
Pin6 - A: 驱动器输出/接收器输入 (同相)
Pin7 - B: 驱动器输出/接收器输入 (反相)
Pin8 - Vcc
注意将AB间120欧姆去掉,如果采用阻抗匹配的电缆,300米以内几乎可以不用加终端电阻。加终端电阻的缺点就是增大了线路的无用功耗,尤其是电池系统供电时,降低了电池的续航能力。
PS: 我一开始没有将它去掉,导致只能发送数据,无法接收数据。
RS-232电平的电气特性
EIA电平(串口)
逻辑1:-3V~-15V
逻辑0:+3V~+15V
TTL电平(TPAD)
逻辑1:+2V~+5V
逻辑0:+0V~+0.8V
接收数据:EIA->TTL 232转TTL
发送数据:TTL->EIA TTL转232
串口异步通信的重要参数:
波特率: bps (bit per second)
数据位的个数: 5 6 7 8
校验方式: 奇校验 偶校验 无校验
停止位: 1bit 2bit
RS485电平 和RS422电平 由于两者均采用 差分传输(平衡传输)的方式,所以他们的电平方式,一般有两个引脚 A,B
发送端 AB间的电压差
+2 ~ +6v 1
-2 ~ -6v 0
接收端 AB间的电压差
大于 +200mv 1
小于 -200mv 0
定义逻辑1为B>A的状态
定义逻辑0为A>B的状态
AB之间的电压差不小于200mv
一对一的接头的情况下:
RS232 可做到双向传输,全双工通讯 最高传输速率 20kbps
422 只能做到单向传输,半双工通讯,最高传输速率10Mbps
485 双向传输,半双工通讯, 最高传输速率10Mbps
起始位是一个值为0的位,所以对于正逻辑的TTL电平,起始位是一位时间的低电平;停止位是值为1的位,所以对于正逻辑的TTL电平,停止位是高电平。线路路空闲或者数据传输结束,对于正逻辑的TTL电平,线路总是1。对于负逻辑(如RS-232电平)则相反。
例如,对于16进制数据55aaH,当采用8位数据位、1位停止位传输时,它在信号线上的波形如图1(TTL电平)和图2(RS-232电平)所示。 (先传第一个字节55,再传第二个字节aa,每个字节都是从低位向高位逐位传输)
图1 TTL电平的串行数据帧格式(55aah)
图2 RS-232电平的串行数据帧格式(55aah)
可以看了,第一个字节的10位(1位起始位,8位数据位和1位停止位)共占约1.05ms,这样可计算出其波特率约为:
10bit / 1.05ms X 1000 ≈ 9600 bit/s
如果上图中的时间轴是100us/格,同样可以计算出波特率应是19200bit/s。
当通讯不正常,又能观察到波形时,就可根据上述方法,从波形图计算一下波特率是否正确。
RS-485发送数据时的正确时序如图4所示。
图4 RS-485的正确发送数据时序
在图4中,发送控制信号的宽度基本与数据信号的宽度一致,所以能保证发送数据的正确和发送后及时转为接收。
图5 和图6 分别是控制信号太短和控制信号太长的情况。
图5 RS-485控制信号太短时的时序
图6 RS-485控制信号太短时的时序
在图5中,由于控制信号关闭过早,则第二个字节的后两位将发送错误;在图6中,由于控制信号关闭过迟,使485芯片在发送数据后,不能及时转到接收状态,此时总线若有数据过来,则本单元将不能正确接收。
总结:只要掌握上述波形分析方法,任何异步串行数据的接收和发送问题,基本都可以得到解决。
-
#include <fcntl.h> //文件控制定义
-
#include <stdio.h> //标准输入输出定义
-
#include <stdlib.h> //标准函数库定义
-
#include <unistd.h> //Unix标准函数定义
-
#include <errno.h> //错误好定义
-
#include <termios.h> //POSIX终端控制定义
-
#include <sys/ioctl.h> //ioctl函数定义
-
#include <string.h> //字符操作
-
#include <sys/types.h>
-
#include <sys/stat.h>
-
#include <pthread.h>
-
#include <sys/timeb.h>
-
-
//时间戳
-
long long getSystemTime() {
-
struct timeb t;
-
ftime(&t);
-
return
1000 * t.time + t.millitm;
-
}
-
-
long
long start;
-
long
long end;
-
-
-
//定义互斥量
-
pthread_mutex_t mutex;
-
int fd_gpio;
-
-
struct termios newtio, oldtio;
-
typedef
struct {
-
int pin_idx;
-
int pin_dir;
-
int pin_sta;
-
} davinci_gio_arg;
-
-
typedef
enum {
-
AT91PIO_DIR_OUT =
0,
-
AT91PIO_DIR_INP
-
} davinci_gio_dir;
-
//驱动判断输入输出模式
-
-
davinci_gio_arg arg;
-
-
#define DEV_PIO_LED "/dev/pio"
-
// 需要手动添加设备号 mknod /dev/pio c 203 0
-
#define PIO_NUM 47
-
// 47pin 为控制输入输出方向引脚
-
#define DEV_UART "/dev/ttyS1"
-
// /dev/ttyS1 为串口设备
-
-
#define IOCTL_PIO_SETDIR 1 //set gpio direct
-
#define IOCTL_PIO_GETDIR 2 //get gpio direct
-
#define IOCTL_PIO_SETSTA 3 //set gpio status
-
#define IOCTL_PIO_GETSTA 4 //get gpio status
-
-
//保存信息
-
int log_init( const char *strFileName )
-
{
-
int fdLog =
-1;
-
-
if(
-1 == (fdLog = open( strFileName, O_CREAT|O_TRUNC ) ) )
-
{
-
}
-
close( fdLog );
-
}
-
-
int log_out( const char *strFileName, const char * szLog )
-
{
-
int fdLog =
-1;
-
-
if(
-1 == ( fdLog = open( strFileName, O_CREAT|O_WRONLY|O_APPEND ) ) )
-
{
-
printf(
"LOG (%s) open error!\n", strFileName );
-
return
-1;
-
}
-
-
write( fdLog, szLog,
strlen( szLog ) );
-
-
close( fdLog );
-
-
return
0;
-
}
-
-
//配置串口
-
/* 参数说明:fd 设备文件描述符,nspeed 波特率,nbits 数据位数(7位或8位),
-
parity 奇偶校验位('n'或'N'为无校验位,'o'或'O'为偶校验,'e'或'E'奇校验),
-
nstop 停止位(1位或2位)
-
成功返回1,失败返回-1。
-
*/
-
int set_com_opt( int fd, int nspeed, int nbits, char parity, int nstop )
-
{
-
char szTmp[
128];
-
//打印配置信息
-
sprintf( szTmp,
"set_com_opt - speed:%d,bits:%d,parity:%c,stop:%d\n",
-
nspeed, nbits, parity, nstop );
-
-
log_out(
"./485.log", szTmp );
-
//保存并测试现在有串口参数设置,在这里如果串口号等出错,会有相关的出错信息
-
if( tcgetattr( fd, &oldtio ) !=
0 )
-
{
-
-
sprintf( szTmp,
"SetupSerial 1" );
-
-
log_out(
"./485.log", szTmp );
-
-
perror(
"SetupSerial 1" );
-
return
-1;
-
}
-
-
//修改输出模式,原始数据输出
-
bzero( &newtio,
sizeof( newtio ));
-
newtio.c_cflag &=~(OPOST);
-
-
//屏蔽其他标志位
-
newtio.c_cflag |= (CLOCAL | CREAD );
-
newtio.c_cflag &= ~CSIZE;
-
-
//设置数据位
-
switch( nbits )
-
{
-
case
7:
-
newtio.c_cflag |= CS7;
-
break;
-
case
8:
-
newtio.c_cflag |= CS8;
-
break;
-
default:
-
perror(
"Unsupported date bit!\n");
-
return
-1;
-
}
-
-
//设置校验位
-
switch( parity )
-
{
-
case
'n':
-
case
'N':
//无奇偶校验位
-
newtio.c_cflag &= ~PARENB;
-
newtio.c_iflag &= ~INPCK;
-
break;
-
case
'o':
-
case
'O':
//设置为奇校验
-
newtio.c_cflag |= ( PARODD | PARENB );
-
newtio.c_iflag |= ( INPCK | ISTRIP );
-
break;
-
case
'e':
-
case
'E':
//设置为偶校验
-
newtio.c_iflag |= ( INPCK |ISTRIP );
-
newtio.c_cflag |= PARENB;
-
newtio.c_cflag &= ~PARODD;
-
break;
-
default:
-
perror(
"unsupported parity\n");
-
return
-1;
-
}
-
-
//设置停止位
-
switch( nstop )
-
{
-
case
1:
-
newtio.c_cflag &= ~CSTOPB;
-
break;
-
case
2:
-
newtio.c_cflag |= CSTOPB;
-
break;
-
default :
-
perror(
"Unsupported stop bit\n");
-
return
-1;
-
}
-
-
//设置波特率
-
switch( nspeed )
-
{
-
case
2400:
-
cfsetispeed( &newtio, B2400 );
-
cfsetospeed( &newtio, B2400 );
-
break;
-
case
4800:
-
cfsetispeed( &newtio, B4800 );
-
cfsetospeed( &newtio, B4800 );
-
break;
-
case
9600:
-
cfsetispeed( &newtio, B9600 );
-
cfsetospeed( &newtio, B9600 );
-
break;
-
case
115200:
-
cfsetispeed( &newtio, B115200 );
-
cfsetospeed( &newtio, B115200 );
-
break;
-
case
460800:
-
cfsetispeed( &newtio, B460800 );
-
cfsetospeed( &newtio, B460800 );
-
break;
-
default:
-
cfsetispeed( &newtio, B9600 );
-
cfsetospeed( &newtio, B9600 );
-
break;
-
}
-
-
//设置等待时间和最小接收字符
-
newtio.c_cc[VTIME] =
0;
-
newtio.c_cc[VMIN] =
0;
-
//VTIME=0,VMIN=0,不管能否读取到数据,read都会立即返回。
-
-
//输入模式
-
newtio.c_lflag &= ~(ICANON|ECHO|ECHOE|ISIG);
-
//设置数据流控制
-
newtio.c_iflag &= ~(IXON|IXOFF|IXANY);
//使用软件流控制
-
//如果发生数据溢出,接收数据,但是不再读取 刷新收到的数据但是不读
-
tcflush( fd, TCIFLUSH );
-
//激活配置 (将修改后的termios数据设置到串口中)
-
if( tcsetattr( fd, TCSANOW, &newtio ) !=
0 )
-
{
-
sprintf( szTmp,
"serial set error!\n" );
-
-
log_out(
"./485.log", szTmp );
-
perror(
"serial set error!" );
-
return
-1;
-
}
-
-
log_out(
"./485.log",
"serial set ok!\n" );
-
return
1;
-
}
-
-
//打开串口并返回串口设备文件描述
-
int open_com_dev( char *dev_name )
-
{
-
int fd;
-
char szTmp[
128];
-
-
log_init(
"./485.log" );
-
if(( fd = open( dev_name, O_RDWR|O_NOCTTY|O_NDELAY)) ==
-1 )
-
{
-
-
perror(
"open\n");
-
//printf("Can't open Serial %s Port!\n", dev_name );
-
sprintf( szTmp,
"Can't open Serial %s Port!\n", dev_name );
-
-
log_out(
"./485.log", szTmp );
-
-
return
-1;
-
}
-
-
sprintf( szTmp,
"open %s ok!\n", dev_name );
-
log_out(
"./485.log", szTmp );
-
-
if(fcntl(fd,F_SETFL,
0)<
0)
-
{
-
printf(
"fcntl failed!\n");
-
}
-
//printf("Open %s ok\n",dev_name );
-
return fd;
-
}
-
-
//发送云台数据
-
void* task(void* p)
-
{
-
char ch;
-
int j =
0, nread =
0;
-
while(
scanf (
"%s", &ch) ==
1)
-
{
-
pthread_mutex_lock (&mutex);
-
arg.pin_sta =
1;
//设为高电平 发送态
-
ioctl(fd_gpio, IOCTL_PIO_SETSTA, &arg);
-
-
int fd = open_com_dev( DEV_UART );
-
if( fd <
0 )
-
{
-
printf(
"open UART device error! %s\n", DEV_UART );
-
}
-
else
-
set_com_opt(fd,
2400,
8,
'n',
1);
-
//set_com_opt(fd, 9600,8,'n',1);
-
-
char buff[] = {
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x11};
-
int len = write(fd,buff,
sizeof (buff));
-
if (len <
0)
-
{
-
perror (
"write err");
-
exit (
-1);
-
}
-
//打印发送数据
-
printf (
"sead: ");
-
for (j =
0; j <
sizeof(buff); j++)
-
{
-
printf (
"%02X ", buff[j]);
-
}
-
printf (
"\n");
-
-
//清除scanf缓冲区
-
scanf (
"%*[^\n]");
-
scanf (
"%*c");
-
close (fd);
-
pthread_mutex_unlock (&mutex);
-
}
-
}
-
-
//单片机数据收发
-
void* task1(void* p)
-
{
-
char buf[
255];
-
int j =
0, res =
0, nread =
0, i =
0;
-
while (
1) {
-
pthread_mutex_lock (&mutex);
-
-
arg.pin_sta =
1;
//设为高电平 发送态
-
ioctl(fd_gpio, IOCTL_PIO_SETDIR, &arg);
-
//打开/dev/pio
-
int fd_s = open_com_dev( DEV_UART );
-
if( fd_s <
0 )
-
{
-
printf(
"open UART device error! %s\n", DEV_UART );
-
}
-
else
-
set_com_opt(fd_s,
2400,
8,
'n',
1);
-
//set_com_opt(fd_s, 9600,8,'n',1);
-
-
//发送数据
-
char buff[] = {
0xaa,
0x55,
0x05,
0x00,
0x33,
0x44,
0x14,
0x90,
0x00};
-
int len = write(fd_s,buff,
sizeof (buff));
-
if (len <
0)
-
{
-
perror (
"write err");
-
exit (
-1);
-
}
-
printf (
"sead: ");
-
for (j =
0; j <
sizeof (buff); j++)
-
{
-
printf (
"%02X ", buff[j]);
-
}
-
printf (
"\n");
-
-
close (fd_s);
-
start=getSystemTime();
-
arg.pin_sta =
0;
//设为低电平 接收态
-
ioctl(fd_gpio, IOCTL_PIO_SETSTA, &arg);
-
int fd_r=open_com_dev( DEV_UART );
-
if( fd_r <
0 )
-
{
-
printf(
"open UART device error! %s\n", DEV_UART );
-
}
-
else
-
set_com_opt(fd_r,
2400,
8,
'n',
1);
-
//set_com_opt(fd_r, 9600,8,'n',1);
-
-
//执行select
-
fd_set rd;
-
FD_ZERO(&rd);
-
FD_SET(fd_r, &rd);
-
if ((res = select (fd_r+
1,&rd,
NULL,
NULL,
NULL) )<
0)
-
{
-
perror (
"read err");
-
exit (
-1);
-
}
-
-
memset (buf,
0,
sizeof (buf));
-
if (FD_ISSET (fd_r, &rd))
-
{
-
//接收数据 8 8 2
-
int res1 =
0;
-
while ((nread = read(fd_r, buf,
8)) >
0)
-
{
-
//打印接收数据
-
for (i =
0; i < nread; i++)
-
{
-
printf (
"%02X ", buf[i]);
-
}
-
//退出循环, 这里有点疑问
-
res1 += nread;
-
if (res1 ==
18)
-
{
-
memset (buf,
0,
sizeof (buf));
-
printf (
"\n");
-
break;
-
}
-
}
-
}
-
close (fd_r);
-
pthread_mutex_unlock (&mutex);
-
end=getSystemTime();
-
printf(
"time: %lld ms\n", end-start);
-
usleep (
200000);
-
}
-
}
-
-
int main (void)
-
{
-
-
int error =
0, error1 =
0;
-
arg.pin_idx = PIO_NUM;
-
arg.pin_dir = AT91PIO_DIR_OUT;
-
//打开/dev/pio设备
-
fd_gpio = open(DEV_PIO_LED, O_RDWR);
-
if(fd_gpio <
0)
-
{
-
perror(
"fd_gpio open err");
-
exit (
-1);
-
}
-
-
//初始化互斥量
-
pthread_mutex_init (&mutex,
0);
-
pthread_t tid, tid1;
-
//创建线程
-
error = pthread_create (&tid,
NULL, task,
NULL);
-
error1 = pthread_create (&tid1,
NULL, task1,
NULL);
-
//等待线程结束
-
pthread_join (tid,
NULL);
-
pthread_join (tid1,
NULL);
-
//销毁互斥量
-
pthread_mutex_destroy(&mutex);
-
//关闭设备
-
close (fd_gpio);
-
return
0;
-
}
虽然以上代码只有三百多行,但是其包含的内容确是很多的,下面就一一的来总结。
一般招聘信息上 都会有这样一项要求。了解Modbus基于RS485,RS232,以太网等总线的通讯协议,熟练操作Modbus相关软件。
上面我们对RS485,RS232硬件做了分析,接下来我们看一下软件上面该如何处理。前面已经提到过Linux下皆为文件,这当然也包括我们今天的主角 UART0 串口。因此对他的一切操作都和文件的操作一样(涉及到了open,read,write,close等文件的基本操作)。
1. 打开串口
2. 串口初始化
3. 读串口或写串口
4. 关闭串口
既然串口在linux中被看作了文件,那么在对文件进行操作前先要对其进行打开操作。
即通过访问/dev/ttyS0,/dev/ttyS1,/dev/ttyS2这些设备文件实现对串口的访问。
==============================
这里有个问题:
你怎么知道访问的是哪个串口?
可以进行一下测试,echo hello > /dev/ttyS0
看看是否有hello输出。
如果串口使用不对,会出现错误:
setup serial:bad file descriptor
set parity Error
==============================
l O_NOCTTY:表示打开的是一个终端设备,程序不会成为该端口的控制终端。如果不使用此标志,任务一个输入(eg:键盘中止信号等)都将影响进程。
l O_NDELAY:表示不关心DCD信号线所处的状态(端口的另一端是否激活或者停止)。不说明这个标志的话,该程序就会在DCD信号线为低电平时停止。
1> 调用open()函数打开串口,获取串口设备文件描述符
2> 获取串口状态,判断是否阻塞
3> 测试打开的文件描述符是否为终端设备
需要设置串口波特率,数据流控制,帧的格式(即数据位个数,停止位,校验位,数据流控制)
1> 设置波特率
2> 设置数据流控制
3> 设置帧的格式(即数据位个数,停止位,校验位)
说明:
1> 设置串口参数时要用到termios结构体,因此先要通过函数 tcgettattr(fd,&options)获得串口指向termios结构的指针。termios.h头文件中定义的termios结构,如下:
2> 通过cfsetispeed函数和cfsetospeed函数用来设置串口的输入/输出波特率。一般情况下,输入和输出波特率相等的。
3> 设置数据位可以通过修改termios机构体中c_flag来实现。其中CS5,CS6,CS7,CS8对应数据位的5,6,7,8。在设置数据位时,必须要用CSIZE做位屏蔽。
以下是几个数据位、停止位和校验位的设置方法:(以下均为1位停止位)
4> 数据流控制是使用何种方法来标志数据传输的开始和结束。
5> 在设置完波特率,数据流控制,数据位,校验位,停止位,停止位后,还要设置最小等待时间和最小接收字符。
6> 在完成配置后要通过tcsetattr()函数来激活配置。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。