基于485总线的评分系统

实操任务

B级任务(80%)                     

要求:

    使用两块STC板分别下载上一节所提供的.hex文件,搭建485双机通信电路,在linux中编程实现打分程序代码。       

步骤:

  1. 阅读程序系统流程框图,明确双机通信的功能需求。

  2. 熟悉上一节中模拟MODBUS协议的数据包结构,相关功能码及附加数据定义

   | 功能码         | 读下位机功能码 | 0X03 |

   | -------------- | -------------- | ---- |

   | 检测功能码     | 0X08           |      |

   | 地址错误功能码 | 0X10           |      |

   | 复位功能码     | 0X01           |      |

   | 附加数据       | 错误码         | 0X6F |

   | 包头           | 0X5A           |      |

   | 广播地址       | 0X00           |      |

   | 自定义内容     | 0X13           |      |

   协议中的检验字节,本打分系统采用累加和编码。

3. 回顾485总线数据收发实验,搭建双机通信电路。参考上一节内容确保STC从机编号和评分设定完成后,按下KEY2、KEY3按键标志,第1位和第8位LED灯被点亮。

4. PC端串口设置如下:

   串口波特率:9600 数据位:8位  校验位:无 停止位:1

5. 所编写的PC端程序应参考上一节中的通信协议完成一次完整的评分过程:

- - 需要包含串口的设置

  - 主节点发起从机检测过程:发送指定从机编号正常检测数据包,判断回应查询数据包是否符合上一节中的通信协议。

  - 主机获取从机评分过程:发送指定从机评分相关数据包,判断回应查询数据包是否符合上一节中的通信协议。

  - 主机发起结束评分的过程,若复位成功,STC从机上第1位和第8位LED灯会熄灭。

  - 展示串口相关信息,展示检测到的从机编号和从机的评分等。

代码设计

#include <cstdio>
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include "serial.h"
#include <setjmp.h>
using namespace std;
typedef unsigned char uchar;


/* 基于RS485总线的评分系统-主机程序-双机评分 */
string format(const vector<uchar> &data);			//数据处理为字符串
int check(serial &pipe, int addr);				//设备检测
int get_score(serial &pipe, int addr);			//获取分数
void reset(serial &pipe);					//从机复位

int main() {
    serial pipe("/dev/ttyUSB0", B9600);
    int addr, check_ret, score;
    while(1){
        cout<<"输入说明:\n输入合法从机地址查询分数\n输入-2将从机复位\n输入-1退出程序\n如果程序无法正确运行,不能接收从机回应,请重启重试\n请输入指令:";
        cin>>addr;
        if(addr == -1) break;
        if(addr == -2) {
            reset(pipe);
            continue;
        }
        check_ret = check(pipe, addr);
        if(check_ret == 1){
            cout<<"设备检测正常"<<endl;
            sleep(1);
    	    if(!(score = get_score(pipe, addr)))
    	        cout<<"获取分数失败"<<endl;
    	    else 
    	        cout<<"分数:"<<score<<endl;
        }
        else if(check_ret == 0){
            cout<<"地址错误,请输入地址重试"<<endl;
            continue;
        }
	else{
	    cout<<"数据传输错误,请重启从机"<<endl;
	}        

        //reset(pipe);
        cout<<"从机已复位,输入-1退出"<<endl;
    }
    return 0;

}
/***************************************************** 
将数据处理为字符串
******************************************************/
string format(const vector<uchar> &data) {
    std::string str(2 * data.size() + 1, '\x00');
    for (int i = 0; i < data.size(); i++) {
        sprintf(&str[i * 2], "%02X", data[i]);
    }
    return str;
}
/***************************************************** 
从机地址检测:
参数:serial串口,从机地址
主机发送进行数据检测:5a + 从机地址 + 检测功能码08 + 13 + 校验码 
返回值:1(地址正确);0(地址错误);-1(数据错误)
******************************************************/
int check(serial &pipe,int addr){
	int check_code = 117 + addr, ret;				//校验码为累加和
	vector<uchar> code = {0x5a, 0x08, 0x13};
	vector<uchar> rec;
	code.insert(code.begin()+1, (uchar)addr);			//插入从机地址
	code.push_back((uchar)check_code);				//插入校验码
	//cout << format(code) << endl;				
	

	/* 写入并接收回应数据包 */
	pipe.myWrite(code);
	sleep(1);
	rec = pipe.myRead(5);
	if(format(rec) == format(code))				//检验接收数据包是否与发送相同,相同则从机地址正确
	    ret = 1;
	else{
	    if(rec[3] == 0x6f) ret = 0;
	    else ret = -1;
	}
	return ret;

}
/***************************************************** 
获取从机分数:
参数:serial串口,从机地址
主机发送进行分数获取:5a + 00 + 读取功能码03 + 从机地址 + 校验码 
返回值:分数(数据正确);-1(从机未准备好);-2(数据错误)
******************************************************/
int get_score(serial &pipe, int addr){
	int check_code = 93 + addr, ret;				//校验码为累加和
	vector<uchar> code = {0x5a, 0x00, 0x03};
	vector<uchar> rec;
	code.insert(code.begin()+3, (uchar)addr);			//插入从机地址
	code.push_back((uchar)check_code);				//插入校验码
	//cout << format(data) << endl;				
	

	/* 写入并接收回应数据包 */
	pipe.myWrite(code);
	sleep(1);
	rec = pipe.myRead(5);
	if(rec[3] == 0x6f) ret = -1;					//检验是否错误
	else {
	    ret = (int)rec[3];						//转为数字
	    if(ret < 0 || ret > 100) ret =-2;				//检测数字是否合法
	}
	return ret;

}
/***************************************************** 
从机复位:
参数:serial串口,从机分数值
主机发送进行从机复位:5a + 广播地址00 + 复位功能码01 + 00 +校验字节
******************************************************/
void reset(serial &pipe){
	vector<uchar> code = {0x5a, 0x00, 0x01,0x00,0x5b};
	//cout <<"reset:"<< format(code) << endl;				
	/* 发送数据包 */
	for(int i=0;i<600;i++) {
	    pipe.myWrite(code);
	}
	return;	    
}
```

```c
//serial.h
#ifndef SERIAL_H
#define SERIAL_H

#include <cstring>
#include <vector>
#include <sys/termios.h>


class serial {
private:
    int board = -1, epfd = -1;
public:
    serial(const char *board_path, speed_t baud_rate);
    ~serial();
    std::vector<unsigned char> myRead(size_t n) const;
    void myWrite(const std::vector<unsigned char> &data) const;
};

#endif //SERIAL_H
```

```c
//serial.c
#include "serial.h"
#include <cerrno>
#include <cstdio>
#include <iostream>
#include <sys/epoll.h>
#include <sys/fcntl.h>
#include <sys/termios.h>
#include <sys/unistd.h>
#include <stdio.h>
#include <signal.h>
#define err_check(code) if ((code) < 0) {    \
    printf("Error: %s\n", strerror(errno));  \
    _exit(1);                                \
}
serial::serial(const char *board_path, speed_t baud_rate) {
    err_check(board = open(board_path, O_RDWR | O_NOCTTY ))

    termios attrs {};
    tcgetattr(board, &attrs);
    // 设置波特率
    err_check(cfsetispeed(&attrs, baud_rate))
    err_check(cfsetospeed(&attrs, baud_rate))
    attrs.c_iflag &= ~( BRKINT | ICRNL | INPCK | ISTRIP | IXON | IXOFF );
    attrs.c_oflag &= ~( OPOST | ONLCR | OCRNL );
    attrs.c_lflag &= ~( ECHO | ICANON | IEXTEN | ISIG );
    attrs.c_cflag &= ~( CSIZE | PARENB );
    attrs.c_cflag |= CS8;
    attrs.c_cc[VMIN]  = 1;
    attrs.c_cc[VTIME] = 0; 
    // 设置终端参数,所有改动立即生效
    err_check(tcsetattr(board, TCSANOW, &attrs))
    // 重新打开设备文件以应用新的终端参数
    close(board);
    err_check(board = open(board_path, O_RDWR | O_NOCTTY)) 
    // 创建一个新的 epoll 实例,并返回一个用于控制的文件描述符
    err_check(epfd = epoll_create(1))
    
    epoll_event event {
        .events = EPOLLIN | EPOLLET,  // 当对端变为可读时触发事件
        .data = {
            .fd = board
        }
    };
    // 将这个事件添加到 epoll 的监听列表中
    err_check(epoll_ctl(epfd, EPOLL_CTL_ADD, board, &event))
}
serial::~serial() {
    (~board) && close(board);
    (~epfd) && close(epfd);
}
std::vector<unsigned char> serial::myRead(size_t n) const {
    size_t count = 0;
    std::vector<unsigned char> buffer(n);

    while (count < n) 
    {
        epoll_event event {};
        // 等待串口对端发来数据
        epoll_wait(epfd, &event, 1, 5000);		// 指定超时值,避免无限期阻塞等待
        // 读取数据,然后根据读取到的数据数量决定是否需要继续读取
        count += ::read(board, &buffer[count], n); 
    }
    //tcflush(board,TCIOFLUSH);
    return buffer;
}
void serial::myWrite(const std::vector<unsigned char> &data) const {
    size_t count = 0;
    //tcflush(board,TCOFLUSH);
    while (count < data.size()) {
        // 向串口写入数据
        count += ::write(board, &data[count], data.size() - count);
}
}

程序设计目标:通过本案例加深理解RS485通信方式,实现上位机的主控制器与所有的下位机进行通信。

程序运行效果说明:通过RS232/RS485转换器将多个带有485模块的下位机控制程序的单片机挂载在总线上。用一块单片机做为上位机,下载上位机接点软件中的hex文件,另外的单片机作为下位机,下载下位机程序。下位机单片机上电后,数码管前两位显示从机编号,后三位显示评分结果。首先按下导航按键的中心按钮进入设置模式,被选中设定的数码管小数点被点亮;然后通过控制导航按键的左右方向实现数码管的位选,上下方向实现数码管上数值的加减,再按一次中心按钮退出设置模式。接着按下KEY2、KEY3按键标志从机编号和评分设定完成,第1位和第8位LED灯被点亮;最后通过控制上位机的主控制器的从机检测和多机评分按钮,获取单片机设定的从机编号和评分,从而实现上位机与下位机的通信。

测试方法

1.通过杜邦线将51单片机与RS232/RS485转换器连接,再通过USB转RS232/RS485串口通讯线与PC机连接,下载hex文件,并给单片机上电;

2.如果直接用某一台单片机做主机,该单片机需要下载上位机程序中的接点软件而不是下位机软件;

3.下位机下载后的初始现象为:最左边两个数码管显示00表示从机编号,最右边3个数码管显示000表示评分;

4.按下导航按键中心按钮进入设置模式,将从机编号和评分设置完成后再按一次中心键退出设置模式,再按下KEY1,KEY2,标志设置完成;

5.通过控制上位机进行从机检测获取下位机编号,并获取其评分,数据显示上位机的主控制器上,最后结束评分,单片机LED灯熄灭。

实验体会

RS232接口在总线上只允许连接1个收发器, 即单站能力。而RS485接口在总线上是允许连接多达128个收发器。即具有多站能力,这样用户可以利用单一的RS485接口方便地建立起设备网络。RS485属于半双工通信,数据可以在一个信号载体的两个方向上传输,但是不能同时进行传输。电平转换采用差分电路方式,A、B两线的电压差大于0.2认为是逻辑“1”,小于-0.2认为是逻辑“0”,只有通信双方一方处于发送,一方处于接收时,通信才能正常进行。RS485广泛运用在工业自动化控制、视频监控、门禁对讲以及楼宇报警等各个领域。

本实验让我们对于RS485和RS232有了更多的了解,明白了它们是怎样工作的。

猜你喜欢

转载自blog.csdn.net/a45667/article/details/125955236