Chaichai
串口通信

串口通信

你会看到,unsigned char的一万种用法(bushi)、串口通信的原理和操作方法、try-catch有什么用?
虽然是AI生成的(),但是我们可以借助它来学习

unsigned char 解释

unsigned char 是C/C++中的一种基本数据类型,表示无符号字符型数据。以下是详细解释:

基本特性

  • 无符号性:只能表示非负整数,范围是0到255(在大多数平台上)
  • 大小:通常占用1字节(8位)内存空间
  • 用途
    • 存储原始字节数据
    • 处理小范围的非负整数
    • 实现位操作和字节级操作

与char和signed char的区别

  • char:可能是signed或unsigned,取决于编译器实现
  • signed char:明确表示有符号字符,范围-128到127
  • unsigned char:明确表示无符号字符,范围0到255

示例代码

1
2
3
4
5
6
unsigned char byte = 200;  // 有效
unsigned char max_val = 255; // 最大值
// unsigned char negative = -1; // 错误,不能赋负值

// 常用于处理二进制数据
unsigned char buffer[1024];

特殊用途

  • 在标准中,unsigned char是唯一保证没有填充位的类型
  • 可用于以字节为单位访问其他对象的内存表示

注意事项

  • 当值超过255时会回绕(即256变为0,257变为1,依此类推)
  • 在算术运算中会被提升为int类型

unsigned char在处理网络协议、文件I/O和低级编程时特别有用。


解释 unsigned char *tmp 和数组赋值

这段代码涉及到指针和内存操作,我来从一个初学者的角度逐步解释:

代码分解

1
2
unsigned char *tmp = (unsigned char *)(&_data_write->shootStatus);
msg[1] = tmp[0];

第一部分:指针声明和赋值

1
unsigned char *tmp = (unsigned char *)(&_data_write->shootStatus);
  1. _data_write->shootStatus:这是一个结构体成员变量,假设shootStatus可能是某种状态值(比如射击状态)

  2. &_data_write->shootStatus:取这个变量的内存地址

  3. (unsigned char *):这是一个强制类型转换,将地址转换为unsigned char类型的指针

  4. unsigned char *tmp:声明一个无符号字符指针变量tmp,并将转换后的地址赋给它

通俗理解:把shootStatus的内存地址当作一个字节数组来看待

第二部分:数组赋值

1
msg[1] = tmp[0];
  1. tmp[0]:访问tmp指针指向的第一个字节(因为tmp被当作字节数组)

  2. msg[1]msg数组的第二个元素(数组索引从0开始)

通俗理解:把shootStatus的第一个字节复制到msg数组的第二个位置

为什么这样做?

这种操作通常用于:

  • 将复杂数据类型(如结构体)转换为原始字节数据
  • 提取数据的第一个字节(可能用于状态标志位)
  • 准备通过网络或串口发送数据(需要字节级别的操作)

实际例子

假设:

  • shootStatus是一个32位整数,值为0xAABBCCDD
  • 内存布局(小端模式):
    • 字节0: 0xDD
    • 字节1: 0xCC
    • 字节2: 0xBB
    • 字节3: 0xAA

那么:

  • tmp[0]就是0xDD
  • 执行后msg[1]就等于0xDD

注意事项

  1. 这种操作依赖于具体的内存布局(大端/小端)
  2. 直接操作内存需要确保类型转换是安全的
  3. 这种代码常见于嵌入式系统或底层编程中

串口通信的基本原理

1. 什么是串口通信?

串口通信是一种逐位传输数据的方式。与并行通信(同时传输多个比特)不同,串口通信一次只传输一个比特。这种方式在长距离传输和低带宽需求的场景中非常有效。

2. 串口通信的关键概念

  • 波特率(Baud Rate)

    • 波特率表示每秒传输的符号数(或比特数)。常见的波特率有 9600、115200 等。
    • 例如,波特率为 9600 表示每秒传输 9600 个比特。
  • 数据位(Data Bits)

    • 每个数据帧中实际传输的数据位数。通常为 7 位或 8 位。
  • 停止位(Stop Bits)

    • 用于标记数据帧结束的位。通常为 1 位或 2 位。
  • 校验位(Parity Bit)

    • 用于简单错误检测的位。可以是奇校验、偶校验或无校验。
  • 流控制(Flow Control)

    • 用于控制数据传输速率的机制,防止数据溢出。常见的流控制方式有硬件流控制(RTS/CTS)和软件流控制(XON/XOFF)。

3. 串口通信的数据帧格式

串口通信的数据通常以帧的形式传输,一个典型的数据帧包括以下几个部分:

  1. 起始位(Start Bit):通常为一个低电平(0),表示数据帧的开始。
  2. 数据位(Data Bits):实际传输的数据,通常为 7 位或 8 位。
  3. 校验位(Parity Bit):可选,用于错误检测。
  4. 停止位(Stop Bits):通常为 1 位或 2 位,表示数据帧的结束。

串口通信的操作方法

1. 硬件连接

串口通信通常使用 RS-232TTL 电平的串口。常见的硬件接口有:

  • DB-9 或 DB-25 接口:用于 RS-232 串口。
  • GPIO 引脚:用于 TTL 串口,常见于单片机和嵌入式设备。

2. 软件配置

在软件中,你需要配置串口的参数,包括波特率、数据位、停止位和校验位等。以下是一个基于 Boost.Asio 的 C++ 示例代码,展示如何配置和使用串口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <boost/asio.hpp>
#include <iostream>

class SerialPort
{
public:
SerialPort(const std::string &port_name, int baud_rate)
{
try
{
// 打开串口
_serial_port = new boost::asio::serial_port(_io_service, port_name);

// 配置串口参数
_serial_port->set_option(boost::asio::serial_port::baud_rate(baud_rate));
_serial_port->set_option(boost::asio::serial_port::flow_control(boost::asio::serial_port::flow_control::none));
_serial_port->set_option(boost::asio::serial_port::parity(boost::asio::serial_port::parity::none));
_serial_port->set_option(boost::asio::serial_port::stop_bits(boost::asio::serial_port::stop_bits::one));
_serial_port->set_option(boost::asio::serial_port::character_size(8));
}
catch (const std::exception &e)
{
std::cerr << "Error: " << e.what() << std::endl;
delete _serial_port;
_serial_port = nullptr;
}
}

~SerialPort()
{
delete _serial_port;
}

void write(const uint8_t *data, size_t size)
{
if (_serial_port)
{
boost::asio::write(*_serial_port, boost::asio::buffer(data, size));
}
}

void read(uint8_t *data, size_t size)
{
if (_serial_port)
{
boost::asio::read(*_serial_port, boost::asio::buffer(data, size));
}
}

private:
boost::asio::io_service _io_service;
boost::asio::serial_port *_serial_port = nullptr;
};

int main()
{
SerialPort serialPort("/dev/ttyUSB0", 115200);

// 发送数据
uint8_t data_to_send[] = {0x01, 0x02, 0x03};
serialPort.write(data_to_send, sizeof(data_to_send));

// 接收数据
uint8_t data_received[10];
serialPort.read(data_received, sizeof(data_received));

// 打印接收到的数据
for (size_t i = 0; i < sizeof(data_received); ++i)
{
std::cout << std::hex << static_cast<int>(data_received[i]) << " ";
}
std::cout << std::endl;

return 0;
}

3. 数据传输

  • 发送数据

    • 使用 write 方法将数据发送到串口。
    • 数据通常是一个字节数组。
  • 接收数据

    • 使用 read 方法从串口读取数据。
    • 读取的数据同样是一个字节数组。

4. 错误处理

  • 在实际应用中,串口通信可能会遇到各种问题,如连接断开、数据错误等。
  • 你需要在代码中添加适当的错误处理逻辑,例如捕获异常并记录日志。

实际应用中的注意事项

  1. 波特率匹配

    • 发送方和接收方的波特率必须一致,否则数据无法正确解析。
  2. 硬件连接

    • 确保硬件连接正确,例如使用正确的串口线或转换器。
  3. 调试工具

    • 使用串口调试工具(如 PuTTY、Minicom 或串口调试助手)可以帮助你测试和调试串口通信。
  4. 线程安全

    • 如果在多线程环境中使用串口,确保对串口操作的线程安全。
  5. 超时处理

    • 在读取数据时,设置适当的超时时间,避免程序卡住。

总结

串口通信是一种简单而有效的通信方式,适用于低速、长距离的数据传输。通过理解其基本原理和操作方法,你可以轻松地在你的项目中实现串口通信。


这是一个很好的问题!在串口通信中,数据的字节序(Byte Order)是一个非常重要的概念。字节序决定了多字节数据(如 int 类型)在内存中的存储顺序。常见的字节序有两种:大端序(Big-Endian)小端序(Little-Endian)

大端序(Big-Endian)

  • 定义:最高有效字节(MSB,Most Significant Byte)存储在最低的内存地址,最低有效字节(LSB,Least Significant Byte)存储在最高的内存地址。
  • 示例:假设有一个 4 字节的整数 0x12345678,在大端序中,它在内存中的存储顺序是:
    • 地址 0: 0x12
    • 地址 1: 0x34
    • 地址 2: 0x56
    • 地址 3: 0x78

小端序(Little-Endian)

  • 定义:最低有效字节(LSB)存储在最低的内存地址,最高有效字节(MSB)存储在最高的内存地址。
  • 示例:假设有一个 4 字节的整数 0x12345678,在小端序中,它在内存中的存储顺序是:
    • 地址 0: 0x78
    • 地址 1: 0x56
    • 地址 2: 0x34
    • 地址 3: 0x12

为什么代码中是倒着的?

在你的代码中,yaw 是一个 4 字节的整数,假设它是 int 类型。代码中通过 tmp 指针逐字节访问 yaw 的值,并将其存储到 msg 数组中。代码如下:

1
2
3
4
5
tmp = (unsigned char *)(&_data_write->yaw);
msg[4] = tmp[3];
msg[5] = tmp[2];
msg[6] = tmp[1];
msg[7] = tmp[0];

这里的关键是 字节序。假设你的系统是小端序(大多数现代计算机,如 x86 架构,都是小端序),那么 yaw 的字节在内存中的存储顺序是:

  • 地址 0: 0x78(LSB)
  • 地址 1: 0x56
  • 地址 2: 0x34
  • 地址 3: 0x12(MSB)

当你通过 tmp 指针访问这些字节时:

  • tmp[0]0x78(LSB)
  • tmp[1]0x56
  • tmp[2]0x34
  • tmp[3]0x12(MSB)

但是,如果你希望在 msg 数组中按照大端序存储这些字节(即 MSB 在前,LSB 在后),你需要将它们倒过来存储:

  • msg[4] 存储 tmp[3](MSB)
  • msg[5] 存储 tmp[2]
  • msg[6] 存储 tmp[1]
  • msg[7] 存储 tmp[0](LSB)

这样,msg 数组中的字节顺序就是:

  • msg[4]: 0x12(MSB)
  • msg[5]: 0x34
  • msg[6]: 0x56
  • msg[7]: 0x78(LSB)

总结

  • 小端序:内存中存储的字节顺序是 LSB -> MSB
  • 大端序:内存中存储的字节顺序是 MSB -> LSB
  • 在代码中,为了将小端序的字节转换为大端序,需要将字节倒过来存储。

实际应用

在实际应用中,你需要根据通信协议的要求来决定使用哪种字节序。如果通信协议要求数据以大端序传输,而你的系统是小端序,就需要在发送数据前将字节顺序转换为大端序。反之亦然。


C++中try-catch的使用方法

try-catch是C++中异常处理的基本机制,用于捕获和处理程序运行时可能出现的异常。下面详细介绍其使用方法:

基本语法结构

1
2
3
4
5
6
7
8
9
try {
// 可能抛出异常的代码块
} catch (异常类型1 变量名) {
// 处理异常类型1的代码
} catch (异常类型2 变量名) {
// 处理异常类型2的代码
} catch (...) {
// 处理所有其他未捕获异常的代码
}

具体使用方法

1. 捕获特定类型异常

1
2
3
4
5
6
7
8
9
try {
int a = 10, b = 0;
if (b == 0) {
throw std::runtime_error("除数不能为零");
}
int c = a / b;
} catch (const std::runtime_error& e) {
std::cerr << "捕获到运行时错误: " << e.what() << std::endl;
}

2. 捕获多种类型异常

1
2
3
4
5
6
7
8
9
try {
// 可能抛出多种异常的代码
} catch (const std::invalid_argument& e) {
// 处理无效参数异常
} catch (const std::out_of_range& e) {
// 处理越界异常
} catch (const std::exception& e) {
// 处理所有标准异常的基类
}

3. 捕获所有异常(catch-all)

1
2
3
4
5
try {
// 可能抛出异常的代码
} catch (...) {
std::cerr << "捕获到未知异常" << std::endl;
}

在您提供的串口代码中的应用

1
2
3
4
5
6
7
8
void SerialPort::serialPortRead(uint8_t *msg, uint8_t max_len)
{
try {
read(*_serial_port, boost::asio::buffer(msg, max_len), _err);
} catch (...) {
LOG(ERROR) << "readData from serial port error! " << _err.message();
}
}

异常处理的最佳实践

  1. 按从具体到一般的顺序捕获异常

    1
    2
    3
    catch (const MySpecificException& e) {...}
    catch (const std::exception& e) {...}
    catch (...) {...}
  2. 避免空的catch块

    1
    2
    3
    4
    5
    6
    7
    8
    // 不好的做法
    catch (...) {}

    // 好的做法
    catch (...) {
    // 至少记录日志
    logError("Unknown exception caught");
    }
  3. 使用标准异常类

    • std::exception - 所有标准异常的基类
    • std::runtime_error - 运行时错误
    • std::logic_error - 逻辑错误
    • std::invalid_argument - 无效参数
    • std::out_of_range - 超出范围
  4. 自定义异常类

    1
    2
    3
    4
    5
    class SerialPortException : public std::runtime_error {
    public:
    SerialPortException(const std::string& msg)
    : std::runtime_error(msg) {}
    };

在串口通信中的典型应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void SerialPort::sendCommand(const Command& cmd)
{
try {
if (!isOpen()) {
throw SerialPortException("Port not open");
}
// 发送命令...
} catch (const SerialPortException& e) {
LOG(ERROR) << "Serial port error: " << e.what();
throw; // 重新抛出给上层处理
} catch (const std::exception& e) {
LOG(ERROR) << "Standard exception: " << e.what();
throw;
}
}

注意事项

  1. 异常处理会有性能开销,不应替代常规错误检查
  2. 不要在析构函数中抛出异常
  3. 确保异常安全 - 即使抛出异常也不会泄漏资源
  4. 考虑使用RAII(资源获取即初始化)技术管理资源

通过合理使用try-catch,可以使您的串口通信代码更加健壮和可靠。

Author:Chaichai
Link:https://chaichai438.github.io/2025/05/15/RM学习/串口通信/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可