C++ CAN总线数据处理框架解析
C++ CAN总线数据处理框架解析
本文将详细解析一个用于CAN总线数据处理的C++框架,重点介绍其中使用的各种C++特性和重要语法。
一、头文件概览
这个头文件(main_public.h
)定义了一个完整的CAN总线数据处理系统,包含了数据结构、同步机制和核心功能函数。下面我们分层解析其中的各个组成部分。
#ifndef MAIN_PUBLIC_H
#define MAIN_PUBLIC_H#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <chrono>
#include <array>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <map>
#include "AUXILIARY_PROPULSION_CAN.h"// 常量定义
constexpr size_t CAN_SIZE = 3; // 目前最多3路CAN诊断
constexpr size_t BUFFER_SIZE = 6000;
constexpr size_t FRAME_SIZE = 100;
constexpr size_t RESULT_BUFFER_SIZE = 6000;
constexpr size_t RESULT_FRAME_SIZE = 10;// CAN接口名称
extern const std::string INTERFACE_NAMES[CAN_SIZE];// 索引数组
extern int write_indices[CAN_SIZE];
extern int read_indices[CAN_SIZE];
extern int local_write_indices[CAN_SIZE];// 时间戳数组
extern std::array<int64_t, CAN_SIZE> current_Timestamp;
extern std::array<int64_t, CAN_SIZE> last_Timestamp;
extern std::array<int32_t, CAN_SIZE> ID;
extern std::array<int64_t, CAN_SIZE> remote_Timestamp1;
extern std::array<int64_t, CAN_SIZE> back_Timestamp1;
extern std::array<int64_t, CAN_SIZE> current_Timestamp_1;
extern std::array<int64_t, CAN_SIZE> last_Timestamp_1;
extern std::array<int64_t, CAN_SIZE> last_Timestamp_2;
extern std::array<int64_t, CAN_SIZE> last_Timestamp_3;// 缓冲区
extern std::array<std::array<std::array<unsigned char, FRAME_SIZE>, BUFFER_SIZE>, CAN_SIZE> g_packet_buffer;
extern std::array<std::array<std::array<unsigned char, RESULT_FRAME_SIZE>, RESULT_BUFFER_SIZE>, CAN_SIZE> Result_Buffer;// 同步工具
extern std::mutex buffer_mutex;
extern std::condition_variable data_condition;
extern std::mutex result_mutex;
extern bool running;// 时间戳记录
extern std::map<size_t, std::map<uint32_t, int64_t>> last_timestamp_map;// 函数声明
int64_t calcFrameInterval(size_t channel, uint32_t frame_id, int64_t current_timestamp);
void parseData();
void summarizeResults();void process_can_data(const struct can_frame &frame, int interface_index);#endif
二、基础类型与标准库组件
1. 基本数据类型
int64_t // 64位有符号整数,用于时间戳存储
int32_t // 32位有符号整数,用于CAN ID存储
uint32_t // 32位无符号整数
uint16_t // 16位无符号整数
uint8_t // 8位无符号整数
size_t // 无符号整数,用于表示大小/索引
2. 标准库容器
std::array // 固定大小数组,替代原生数组
std::map // 关联容器,用于键值对存储
std::string // 字符串类
3. 多线程支持
std::mutex // 互斥锁
std::condition_variable // 条件变量
std::thread // 线程类
三、核心数据结构解析
1. 常量定义
constexpr size_t CAN_SIZE = 3; // 编译时常量,表示CAN通道数量
constexpr size_t BUFFER_SIZE = 6000; // 缓冲区大小
constexpr
表示这些值在编译时即可确定,编译器会进行优化。
区别与联系:
const
保证运行时不可修改,值可在运行时确定constexpr
要求编译时必须确定值,用于编译期计算- 所有
constexpr
都隐含const
特性,但const
不一定是constexpr
小例子:
const int x = rand(); // 合法,运行时确定
constexpr int y = 10 * 2; // 合法,编译时计算(20)
constexpr int z = x; // 错误!x不是常量表达式
2. 多维数组容器
// 三维数组:CAN通道 × 缓冲区大小 × 每帧大小
std::array<std::array<std::array<unsigned char, FRAME_SIZE>, BUFFER_SIZE>, CAN_SIZE> g_packet_buffer;
这种嵌套的std::array
提供了:
- 类型安全
- 固定大小保证
- 方便的迭代器访问
- 边界检查(通过at()方法)
3. 时间戳管理
std::array<int64_t, CAN_SIZE> current_Timestamp; // 每个通道的当前时间戳
std::map<size_t, std::map<uint32_t, int64_t>> last_timestamp_map; // 通道+ID的时间戳记录
使用std::map
实现了两级查找表,用于记录每个通道上每个CAN ID的最后时间戳。
四、同步机制详解
1. 互斥锁与条件变量
extern std::mutex buffer_mutex; // 保护缓冲区的互斥锁
extern std::condition_variable data_condition; // 数据到达通知
典型的生产者-消费者模式:
- 生产者(
process_can_data
)获取数据后通知 - 消费者(
parseData
)等待条件变量
2. 原子标志
extern bool running; // 程序运行控制标志
在多线程环境中,对bool的读写通常是原子的,但更严谨的做法是使用std::atomic<bool>
五、关键函数分析
1. CAN数据处理函数
void process_can_data(const struct can_frame &frame, int interface_index);
参数说明:
const can_frame &
: CAN帧的常量引用,避免拷贝interface_index
: CAN接口索引(0-2)
2. 时间间隔计算
int64_t calcFrameInterval(size_t channel, uint32_t frame_id, int64_t current_timestamp);
实现了帧间隔计算功能,返回微秒级间隔。
3. 解析与汇总线程
void parseData(); // 数据解析线程函数
void summarizeResults(); // 结果汇总线程函数
六、重要C++特性应用
1. 类型别名与常量表达式
constexpr size_t FRAME_SIZE = 100; // 编译期常量
2. 现代C++数组管理
使用std::array
替代原生数组:
- 避免数组到指针的隐式转换
- 提供size()等成员函数
- 支持范围for循环
3. 线程安全设计
通过mutex
+condition_variable
实现:
- 数据缓冲区的线程安全访问
- 高效的通知机制
- 避免忙等待
七、性能考量
- 内存布局:多维
std::array
在内存中是连续的,有利于缓存利用 - 零拷贝设计:使用引用传递CAN帧,避免数据拷贝
- 时间精度:使用纳秒级时间戳(
std::chrono::nanoseconds
) - 缓冲区设计:循环缓冲区实现(通过模运算管理索引)
八、其他
- 使用
std::atomic
替代普通bool作为运行标志 - 考虑使用
std::shared_mutex
实现读写锁 - 添加异常处理机制
- 实现配置文件加载,使参数可配置
这个框架展示了现代C++在嵌入式系统开发中的强大能力,结合了类型安全、高性能和清晰的抽象。
拓展:C++ 中 array 和 vector 的用法与区别
1、基本概念
std::array
- 固定大小的序列容器,大小在编译时确定
- 封装了C风格数组,提供STL容器接口
- 存储在栈上(除非作为对象的一部分分配在堆上)
std::vector
- 动态大小的序列容器,可在运行时调整大小
- 数据存储在堆上
- 提供自动内存管理
2、主要区别
特性 | std::array | std::vector |
---|---|---|
大小 | 固定,编译时确定 | 动态,运行时可变 |
内存位置 | 通常栈上 | 堆上 |
内存管理 | 自动(栈分配) | 自动(堆分配) |
性能 | 更快(无动态分配) | 稍慢(可能涉及重新分配) |
适用场景 | 已知大小的数据集合 | 大小不确定或需要变化的数据集合 |
3、基本用法示例
1) std::array 示例
#include <array>
#include <iostream>int main() {// 创建并初始化std::array<int, 5> arr = {1, 2, 3, 4, 5};// 访问元素std::cout << "第一个元素: " << arr[0] << std::endl;std::cout << "最后一个元素: " << arr.back() << std::endl;// 迭代std::cout << "所有元素: ";for(int num : arr) {std::cout << num << " ";}std::cout << std::endl;// 大小固定std::cout << "数组大小: " << arr.size() << std::endl;return 0;
}
2) std::vector 示例
#include <vector>
#include <iostream>int main() {// 创建空vectorstd::vector<int> vec;// 添加元素vec.push_back(10);vec.push_back(20);vec.push_back(30);// 访问元素std::cout << "第一个元素: " << vec[0] << std::endl;std::cout << "最后一个元素: " << vec.back() << std::endl;// 迭代std::cout << "所有元素: ";for(int num : vec) {std::cout << num << " ";}std::cout << std::endl;// 大小可变vec.pop_back(); // 移除最后一个元素std::cout << "当前大小: " << vec.size() << std::endl;// 调整大小vec.resize(5, 100); // 调整为大小5,新元素初始化为100std::cout << "调整后大小: " << vec.size() << std::endl;return 0;
}
4、选择建议
使用 std::array 当:
- 数据大小在编译时已知且固定
- 需要更高的性能(避免堆分配)
- 需要容器特性但保持类似数组的性能
使用 std::vector 当:
- 数据大小在运行时才能确定
- 需要频繁添加或删除元素
- 需要自动管理内存
- 数据量可能很大(避免栈溢出)
5、性能注意事项
- std::array 访问速度与原生数组相当,没有额外开销
- std::vector 的
push_back
操作可能导致重新分配,使用reserve()
预分配可以优化 - 对于小型数据集,
std::array
通常更高效 - 大型数据集应使用
std::vector
以避免栈溢出
6、高级用法
std::array 的高级初始化
std::array<std::string, 3> colors = {"Red", "Green", "Blue"};
std::vector 的容量管理
std::vector<int> numbers;
numbers.reserve(100); // 预分配空间,避免多次重新分配
for(int i = 0; i < 100; ++i) {numbers.push_back(i);
}
两者都支持STL算法:
#include <algorithm>std::array<int, 5> arr = {5, 3, 1, 4, 2};
std::sort(arr.begin(), arr.end()); // 排序
拓展二:extern
关键字详解
extern
是 C/C++ 中的一个存储类说明符,主要用于声明变量或函数的外部链接性。它在多文件编程中扮演着重要角色。
1、基本概念
extern
的主要用途是:
- 声明但不定义变量或函数
- 指示编译器该变量/函数在其他编译单元中定义
- 实现多个源文件共享同一个变量或函数
2、典型用法
1) 声明外部变量(最常用)
头文件 (globals.h)
extern int globalVar; // 声明(非定义)
源文件A (file1.cpp)
#include "globals.h"
int globalVar = 42; // 实际定义
源文件B (file2.cpp)
#include "globals.h"
void foo() {globalVar = 10; // 使用在file1.cpp中定义的变量
}
2) 声明外部函数
头文件 (utils.h)
extern void helperFunction(); // 声明外部函数
源文件 (utils.cpp)
#include "utils.h"
void helperFunction() { // 实际定义// 函数实现
}
3) 与 const 结合使用
extern const int MAX_SIZE; // 声明外部常量
3、重要特性
-
链接性:
extern
表示变量/函数具有外部链接(external linkage),可以被其他文件访问 -
存储持续时间:不影响变量的存储持续时间(全局变量始终是静态存储期)
-
与 static 对比:
extern
:扩大作用域到其他文件static
:限制作用域在当前文件
-
默认情况:
- 函数默认是
extern
的 - 全局变量默认是
extern
的 const
全局变量默认是static
的(C++中)
- 函数默认是
4、使用场景
-
共享全局变量:多个源文件需要访问同一个全局变量时
-
分离式编译:
- 声明放在头文件中
- 定义放在一个源文件中
- 其他文件通过包含头文件使用
-
跨文件函数调用:当一个源文件需要调用另一个源文件中定义的函数时
5、注意事项
-
避免滥用:过度使用全局变量(即使是
extern
的)会降低代码可维护性 -
初始化:
extern int x; // 声明 int x = 10; // 定义并初始化 extern int y = 5; // 定义(不推荐,会抵消extern作用)
-
C与C++区别:
- 在C中,
const
全局变量默认有外部链接 - 在C++中,
const
全局变量默认有内部链接(需要用extern
显式指定外部链接)
- 在C中,
-
头文件保护:使用
#ifndef
防止多次包含导致的重复声明
6、现代C++中的替代方案
虽然extern
仍然有用,但现代C++更推荐:
- 使用命名空间组织全局变量
- 使用单例模式替代全局变量
- 尽量减少全局变量的使用
7、综合示例
config.h
#ifndef CONFIG_H
#define CONFIG_Hextern const char* APP_NAME; // 声明
extern int debugLevel; // 声明#endif
config.cpp
#include "config.h"const char* APP_NAME = "MyApp"; // 定义
int debugLevel = 1; // 定义
main.cpp
#include "config.h"
#include <iostream>int main() {std::cout << "Running " << APP_NAME << std::endl;if(debugLevel > 0) {std::cout << "Debug mode enabled" << std::endl;}return 0;
}
在这个例子中,APP_NAME
和debugLevel
在config.cpp
中定义,在main.cpp
中通过包含头文件并使用extern
声明来访问。