关于串口通信的那些事

前言:学习java时长两年半,还会rap篮球,作为科班出身,毕业时最终是没赶上时代红利,一年经验3k,况且我还没经验。在这个内卷的时代,最终选择换个赛道。如果巅峰留不住,果断选择进厂包吃包住。
在这里插入图片描述

进入电气自动化行业,最基础的就是先要把通信整明白,软件和硬件之间的通信基本上都采用串口的方式或则网络通信,通信方式和通信协议是不一样的。不明白原理只会用是不行的,要知其然知其所以然。工作了也快一年了,也学了一些东西。今天就来讲讲我曾经踩过的一些坑,给新入行的小白好好捋一捋。232,485有什么区别?硬件接收的数据格式有哪些?发送的数据硬件如何解析?如何更高效的进行串口通信?

什么是串口通信:是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通讯方式。

串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。

串口按电气标准及协议来划分,包括RS-232-C、RS-422、RS485等。

串口通讯的数据格式

一个字符一个字符地传输,每个字符一位一位地传输,并且传输一个字符时,总是以“起始位”开始,以“停止位”结束,字符之间没有固定的时间间隔要求。

每一个字符的前面都有一位起始位(低电平),字符本身由7位数据位组成,接着字符后面是一位校验位(检验位可以是奇校验、偶校验或无校验位),最后是一位或一位半或二位停止位,停止位后面是不定长的空闲位,停止位和空闲位都规定为高电平。实际传输时每一位的信号宽度与波特率有关,波特率越高,宽度越小,在进行传输之前,双方一定要使用同一个波特率设置。

通讯方式

单工模式(Simplex Communication)的数据传输是单向的。通信双方中,一方固定为发送端,一方则固定为接收端。信息只能沿一个方向传输,使用一根传输线。

半双工模式(Half Duplex)通信使用同一根传输线,既可以发送数据又可以接收数据,但不能同时进行发送和接收。数据传输允许数据在两个方向上传输,但是,在任何时刻只能由其中的一方发送数据,另一方接收数据。因此半双工模式既可以使用一条数据线,也可以使用两条数据线。半双工通信中每端需有一个收发切换电子开关,通过切换来决定数据向哪个方向传输。因为有切换,所以会产生时间延迟,信息传输效率低些。

全双工模式(Full Duplex)通信允许数据同时在两个方向上传输。因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。在全双工模式中,每一端都有发送器和接收器,有两条传输线,信息传输效率高。

显然,在其它参数都一样的情况下,全双工比半双工传输速度要快,效率要高。

偶校验与奇校验

在标准ASCII码中,其最高位(b7)用作奇偶校验位。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。

停止位

停止位是按长度来算的。串行异步通信从计时开始,以单位时间为间隔(一个单位时间就是波特率的倒数),依次接受所规定的数据位和奇偶校验位,并拼装成一个字符的并行字节;此后应接收到规定长度的停止位“1”。所以说,停止位都是“1”,1.5是它的长度,即停止位的高电平保持1.5个单位时间长度。一般来讲,停止位有1,1.5,2个单位时间三种长度。

波特率

波特率就是每秒钟传输的数据位数。

波特率的单位是每秒比特数(bps),常用的单位还有:每秒千比特数Kbps,每秒兆比特数Mbps。串口典型的传输波特率600bps,1200bps,2400bps,4800bps,9600bps,19200bps,38400bps。

介绍完串口通信的基本知识后,我们开始言归正传上代码

操作方式一般是以下四个步骤
1、打开串口 分同步和异步两种方式
2、配置串口
3、读写串口
4、关闭串口

这里采用windowsAPI来进行实现

#include <iostream>
#include <string>
#include <windows.h>

//打开串口
HANDLE openPort(LPCSTR portName, DWORD baudrate, BYTE parity, BYTE databit, BYTE stopbit, const DWORD timeout)
{
	// 检查串口名称是否为空
    if (portName == nullptr || *portName == '\0') {
        std::wcerr << "串口名称为空" << std::endl;
        return INVALID_HANDLE_VALUE;
    }
    // 尝试打开串口
    HANDLE hSerial = CreateFileA(portName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (hSerial == INVALID_HANDLE_VALUE) {
    	DWORD error = GetLastError();
        if (error  == ERROR_FILE_NOT_FOUND) {
            std::wcout << portName << "串口未找到" << std::endl;
        }
        else {
            std::wcout << portName << "无法打开串口,错误码:" << error" << std::endl;
        }
        return INVALID_HANDLE_VALUE;
    }
	// 获取并设置串口参数
    DCB dcbSerialParams = { 0 };
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
    if (!GetCommState(hSerial, &dcbSerialParams)) {
        std::wcout << portName << "获取串口状态失败" << std::endl;
        CloseHandle(hSerial);
        return INVALID_HANDLE_VALUE;
    }
	
    dcbSerialParams.BaudRate = baudrate;    // 设置波特率
    dcbSerialParams.Parity = parity;        // 设置奇偶校验位
    dcbSerialParams.ByteSize = databit;    // 设置数据位
    dcbSerialParams.StopBits = stopbit;    // 设置停止位
    if (!SetCommState(hSerial, &dcbSerialParams)) {
        std::wcout << portName << "设置串口状态失败" << std::endl;
        CloseHandle(hSerial);
        return INVALID_HANDLE_VALUE;
    }
	// 设置串口超时时间
    COMMTIMEOUTS timeouts = { 0 };
    timeouts.ReadIntervalTimeout = timeout;
    timeouts.ReadTotalTimeoutConstant = timeout;
    timeouts.ReadTotalTimeoutMultiplier = timeout;
    timeouts.WriteTotalTimeoutConstant = timeout;
    timeouts.WriteTotalTimeoutMultiplier = timeout;

    if (!SetCommTimeouts(hSerial, &timeouts)) {
        std::cout << "设置超时失败" << std::endl;
        CloseHandle(hSerial);
        return INVALID_HANDLE_VALUE;
    }
    std::wcout << portName << "串口打开成功" << "\n";
    return hSerial;
}

//关闭串口
void closePort(HANDLE hSerial)
{
    CloseHandle(hSerial);
}

注意:这个函数返回的是成功打开串口的串口句柄hSerial,是和你打开的设备一一绑定的,后面对硬件的操作都是对句柄的操作

// 将十六进制字符串转换为字节类型数据
std::vector<unsigned char> HexToBytes(const std::string& hexString) {
    std::vector<unsigned char> bytes;

    for (size_t i = 0; i < hexString.length(); i += 2) {
        std::string byteString = hexString.substr(i, 2);
        unsigned char byte = static_cast<unsigned char>(std::stoul(byteString, nullptr, 16));
        bytes.push_back(byte);
    }

    return bytes;
}
// 将字节数据转换为十六进制字符串
std::string BytesToHex(const std::vector<unsigned char>& bytes) {
    std::ostringstream oss;
    oss << std::hex << std::setfill('0');
    for (unsigned char byte : bytes) {
        oss << std::setw(2) << static_cast<int>(byte);
    }
    return oss.str();
}

//写串口
void sendData(HANDLE hSerial, CONST CHAR* hexData) {
    std::vector<unsigned char> hexCMD = HexToBytes(hexData);//注意这里是将hex字符串形式数据转成字节类型,根据需求使用
    DWORD bytesWritten;
	bool JudgeWrite = WriteFile(hSerial, hexCMD.data(), hexCMD.size(), &bytesWritten, NULL);
	if (!JudgeWrite)//写失败
	{
        // 获取错误代码
        DWORD dwError = GetLastError();
		if (GetLastError() == ERROR_IO_PENDING) //重叠 I/O 操作在进行中。
		{
            OVERLAPPED osWrite = { 0 };
            bool bWrite = GetOverlappedResult(hSerial, &osWrite, &bytesWritten, TRUE);
		}
	}
}

//读串口
std::string recvData(HANDLE hSerial)
{
    std::vector<unsigned char> recvData(1024);
    DWORD bytesRead;
    OVERLAPPED osReader = { 0 };
    if (!ReadFile(hSerial, recvData.data(), recvData.size(), &bytesRead, NULL))
    {
        // 获取错误代码
        DWORD dwError = GetLastError();
        // 如果是重叠 I/O 操作在进行中
        if (dwError == ERROR_IO_PENDING)
        {
            // 等待读取操作完成
            bool bRead = GetOverlappedResult(hSerial, &osReader, &bytesRead, TRUE);
        }
    }
    recvData.resize(bytesRead);
    std::string hex_data = BytesToHex(recvData);//这里是将读回来的字节类型转换为字符串类型
    //std::cout << "recv:" << hex_data << "\n";
    return hex_data;
}

这里有个地方困惑了我很久,写串口支持什么样的数据类型?
例如一段这样的指令 01 06 00 02 00 00 28 0A
1、直接以char数组的方式写入
2、字符串的方式写入
这两种我都尝试了,都失败了。
这里让大家见笑了,由于计算机操作系统的知识不够扎实才会有这样的困惑,后面查阅资料深入底层原理才彻彻底底弄明白了。

首先计算机系统中最小的可寻址内存单元通常是字节
因此,使用字节作为基本单位可以直接映射到计算机系统的内存结构中
在计算机中,字节是最小的可寻址的内存单位,通常用于存储和传输数据。它可以表示范围在0到255之间的整数值(对于无符号字节)。由于它的大小是固定的,因此字节经常被用作存储和传输数据的基本单位。
所以我们需要传入字节类型的数据即BYTE类型,将以上指令做以下处理
BYTE cmd[1024] = “\x01 \x06 \x00 \x02 \x00 \x00 \x28 \x0A”
这样就可以字节将cmd传入WriteFile函数中即可生效

这里给大家深入讲解下计算机是如何解析你发送过来的数据
计算机中所有数据都是以二进制保存只有0和1

字符串都是以ASCII码的形式保存在计算机里面的

无论下位机发送的是何种类型的数据,从串口接收的时候,总是将8位二进制数作为字符的ASC码解读。
电脑发送和单片机接收都是该字符的asii码

例如我们需要像设备发送一个字母A,字母A对应的ASCII是65
对应二进制就是0100 0001对应以下波形图
由于波形图是由低位到高位依次排列,所以正好和对应的八位二进制相反
字母A对应的数据帧
初来乍到,有不足的或者错误的地方还请大家指出

  • 51
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值