RS485串口编程

故事扮演 提交于 2020-02-25 20:08:10

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 串口编程

前面已经提到过Linux下皆为文件,这当然也包括我们今天的主角 UART0 串口。因此对他的一切操作都和文件的操作一样(涉及到了open,read,write,close等文件的基本操作)。

(一)Linux下的串口编程又那几部分组成

 

 

1.    打开串口

2.    串口初始化

3.    读串口或写串口

4.    关闭串口

(二)串口的打开

既然串口在linux中被看作了文件,那么在对文件进行操作前先要对其进行打开操作。

1.在Linxu中,串口设备是通过串口终端设备文件来访问的

即通过访问/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信号线为低电平时停止。

3.打开串口模块有那及部分组成

1> 调用open()函数打开串口,获取串口设备文件描述符

2> 获取串口状态,判断是否阻塞

3> 测试打开的文件描述符是否为终端设备

 

4程序:

/***************************************************************** 

 
  1. * 名称: UART0_Open

  2. * 功能: 打开串口并返回串口设备文件描述

  3. * 入口参数: fd :文件描述符 port :串口号(ttyS0,ttyS1,ttyS2)

  4. * 出口参数: 正确返回为1,错误返回为0

  5. *****************************************************************/

  6. int UART0_Open(int fd,char* port)

  7. {

  8.  
  9. fd = open( port, O_RDWR|O_NOCTTY|O_NDELAY);

  10. if (FALSE == fd)

  11. {

  12. perror("Can't Open Serial Port");

  13. return(FASLE);

  14. }

  15. //判断串口的状态是否为阻塞状态

  16. if(fcntl(fd, F_SETFL, 0) < 0)

  17. {

  18. printf("fcntl failed!/n");

  19. return(FALSE);

  20. }

  21. else

  22. {

  23. printf("fcntl=%d/n",fcntl(fd, F_SETFL,0));

  24. }

  25. //测试是否为终端设备

  26. if(0 == isatty(STDIN_FILENO))

  27. {

  28. printf("standard input is not a terminal device/n");

  29. return(FALSE);

  30. }

  31. else

  32. {

  33. printf("isatty success!/n");

  34. }

  35. printf("fd->open=%d/n",fd);

  36. return fd;

  37. }

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!