1.1 单工、半双工、全双工
首先,我使用的是芯片为 SP3485E 为半双工通信。那么先要明确什么是单工、半双工、全双工。
单工数据传输只支持数据在一个方向上传输;
半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;
全双工数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
网卡的全双工(Full Duplex)是指网卡在发送数据的同时也能够接收数据,两者同步进行,这好像我们平时打电话一样,说话的同时也能够听到对方的声音。目前的网卡一般都支持全双工。
提到全双工,就不能不提与之密切对应的另一个概念,那就是“半双工(Half Duplex)”,
所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子,一条窄窄的马路,同时只能有一辆车通过,
当目前有两量车对开,这种情况下就只能一辆先过,等到头儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台。
1.2 关于RS485通信
RS232 标准是诞生于 RS485 之前的,但是 RS232 有几处不足的地方:
接口的信号电平值较高,达到十几 V,使用不当容易损坏接口芯片,电平标准也与TTL 电平不兼容。
传输速率有局限,不可以过高,一般到一两百千比特每秒(Kb/s)就到极限了。
接口使用信号线和 GND 与其它设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且抗干扰性能也比较弱。
传输距离有限,最多只能通信几十米。
通信的时候只能两点之间进行通信,不能够实现多机联网通信。
针对 RS232 接口的不足,就不断出现了一些新的接口标准,RS485 就是其中之一,它具备以下的特点:
采用差分信号。我们在讲 A/D 的时候,讲过差分信号输入的概念,同时也介绍了差分输入的好处,最大的优势是可以抑制共模干扰。
尤其当工业现场环境比较复杂,干扰比较多时,采用差分方式可以有效的提高通信可靠性。RS485 采用两根通信线,通常用 A 和 B 或者 D+和 D-来表示。逻辑“1”以两线之间的电压差为+(0.2~6)V 表示,逻辑“0”以两线间的电压差为-(0.2~6)V 来表示,是一种典型的差分通信。
RS485 通信速率快,最大传输速度可以达到 10Mb/s 以上。
RS485 内部的物理结构,采用的是平衡驱动器和差分接收器的组合,抗干扰能力也大大增加。
传输距离最远可以达到 1200 米左右,但是它的传输速率和传输距离是成反比的,只有在 100Kb/s 以下的传输速度,才能达到最大的通信距离,如果需要传输更远距离可以使用中继。
可以在总线上进行联网实现多机通信,总线上允许挂多个收发器,从现有的 RS485芯片来看,有可以挂 32、64、128、256 等不同个设备的驱动器。
RS485 的接口非常简单,与 RS232 所使用的 MAX232 是类似的,只需要一个 RS485转换器,就可以直接与单片机的 UART 串口连接起来,并且使用完全相同的异步串行通信协议。
但是由于 RS485 是差分通信,因此接收数据和发送数据是不能同时进行的,也就是说它是一种半双工通信。RS485为差分通信:最大的优势是可以抑制共模干扰。
1.3 关于sp3485硬件分析
上图为SP3485原理图
(1)引脚说明:
Pin1 - RO: 接收器输出
Pin2 - RE#:接收器输出使能 (低电平有效)
Pin3 - DE: 驱动器输出使能 (高电平有效)
Pin4 - DI: 驱动器输入
Pin5 - GND: 连接地
Pin6 - A: 驱动器输出/接收器输入 (同相)
Pin7 - B: 驱动器输出/接收器输入 (反相)
Pin8 - Vcc
注意将AB间120欧姆去掉,如果采用阻抗匹配的电缆,300米以内几乎可以不用加终端电阻。加终端电阻的缺点就是增大了线路的无用功耗,尤其是电池系统供电时,降低了电池的续航能力。
PS: 我一开始没有将它去掉,导致只能发送数据,无法接收数据。
(2)电气特性
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
(3)串行数据的格式
异步串行数据的一般格式是:起始位+数据位+停止位,(8-N-1格式) 其中起始位1 位,数据位可以是5、6、7、8位,停止位可以是1、1.5、2位。
起始位是一个值为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)
(4)根据波形图计算波特率
如图3是图1在示波器中的显示示意,其中灰色线是示波器的时间分度线,此时假设是200us/格。
图3 波特率计算示意图
可以看了,第一个字节的10位(1位起始位,8位数据位和1位停止位)共占约1.05ms,这样可计算出其波特率约为:
10bit / 1.05ms X 1000 ≈ 9600 bit/s
如果上图中的时间轴是100us/格,同样可以计算出波特率应是19200bit/s。
当通讯不正常,又能观察到波形时,就可根据上述方法,从波形图计算一下波特率是否正确。
(5)根据波形图判断RS-485收发数据的正确与否
RS-485是一种半双工的串行通讯方式(RS-422为全双工),485电平芯片所以要正确接收和发送数据,必需保证控制信号和数据的同步,否则要么发送数据丢失,要么接收数据可能丢失。
RS-485发送数据时的正确时序如图4所示。
图4 RS-485的正确发送数据时序
在图4中,发送控制信号的宽度基本与数据信号的宽度一致,所以能保证发送数据的正确和发送后及时转为接收。
图5 和图6 分别是控制信号太短和控制信号太长的情况。
图5 RS-485控制信号太短时的时序
图6 RS-485控制信号太短时的时序
在图5中,由于控制信号关闭过早,则第二个字节的后两位将发送错误;在图6中,由于控制信号关闭过迟,使485芯片在发送数据后,不能及时转到接收状态,此时总线若有数据过来,则本单元将不能正确接收。
总结:只要掌握上述波形分析方法,任何异步串行数据的接收和发送问题,基本都可以得到解决。
二、串口通信
2.1 串口的操作一般都通过四个步骤来完成:
1、打开串口
2、配置串口:对串口的波特率、数据位、停止位、校验码、等进行设置。
3、读写串口
4、关闭串口
2.2 完整代码:
#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硬件做了分析,接下来我们看一下软件上面该如何处理。
主要分为下面部分来讲:
(1)串口编程详解
参看:Linux串口编程详解
参看:Linux 串口编程
前面已经提到过Linux下皆为文件,这当然也包括我们今天的主角 UART0 串口。因此对他的一切操作都和文件的操作一样(涉及到了open,read,write,close等文件的基本操作)。
(一)Linux下的串口编程又那几部分组成
1. 打开串口
2. 串口初始化
3. 读串口或写串口
4. 关闭串口
(二)串口的打开
既然串口在linux中被看作了文件,那么在对文件进行操作前先要对其进行打开操作。
即通过访问/dev/ttyS0,/dev/ttyS1,/dev/ttyS2这些设备文件实现对串口的访问。
==============================
这里有个问题:
你怎么知道访问的是哪个串口?
可以进行一下测试,echo hello > /dev/ttyS0
看看是否有hello输出。
如果串口使用不对,会出现错误:
setup serial:bad file descriptor
set parity Error
==============================
2.调用open()函数来代开串口设备,对于串口的打开操作,必须使用O_NOCTTY参数。
l O_NOCTTY:表示打开的是一个终端设备,程序不会成为该端口的控制终端。如果不使用此标志,任务一个输入(eg:键盘中止信号等)都将影响进程。
l O_NDELAY:表示不关心DCD信号线所处的状态(端口的另一端是否激活或者停止)。不说明这个标志的话,该程序就会在DCD信号线为低电平时停止。
1> 调用open()函数打开串口,获取串口设备文件描述符
2> 获取串口状态,判断是否阻塞
3> 测试打开的文件描述符是否为终端设备
/*****************************************************************
-
* 名称: UART0_Open
-
* 功能: 打开串口并返回串口设备文件描述
-
* 入口参数: fd :文件描述符 port :串口号(ttyS0,ttyS1,ttyS2)
-
* 出口参数: 正确返回为1,错误返回为0
-
*****************************************************************/
-
int UART0_Open(int fd,char* port)
-
{
-
fd = open( port, O_RDWR|O_NOCTTY|O_NDELAY);
-
if (FALSE == fd)
-
{
-
perror("Can't Open Serial Port");
-
return(FASLE);
-
}
-
//判断串口的状态是否为阻塞状态
-
if(fcntl(fd, F_SETFL, 0) < 0)
-
{
-
printf("fcntl failed!/n");
-
return(FALSE);
-
}
-
else
-
{
-
printf("fcntl=%d/n",fcntl(fd, F_SETFL,0));
-
}
-
//测试是否为终端设备
-
if(0 == isatty(STDIN_FILENO))
-
{
-
printf("standard input is not a terminal device/n");
-
return(FALSE);
-
}
-
else
-
{
-
printf("isatty success!/n");
-
}
-
printf("fd->open=%d/n",fd);
-
return fd;
-
}
来源:CSDN
作者:布施
链接:https://blog.csdn.net/qq_33611327/article/details/104500909