几个关键点.
结构体要注意4字节对齐
如果你的结构体里用了大量的uint8_t和uint16_t, 则要注意这个问题. 所以为了规避这个问题我将结构体的传递类型都改成了4字节的类型, 一是配置类型的参数多数为 float 和uint32 类型. 很少有int32. 另外一个是配置类型的接口并不需要大量的重复调用, 不在乎传递效率.
一次传递大量的数据块和一次传递一个配置项那个更好?
实践发现还是一次传递大块数据, 将所有配置都发过去为最好. 如果参数实在是太多. 一次发送会占用大量的空间. 那还是建议改成一次传递一个配置数据的小数据包为妙. 我这个目前配置量大概在60个项目上. 这参数写起来要命的.
c
#pragma pack(push, 1) //避免字节对齐
typedef struct {
uint8_t header1; // 帧头1 0xFE
uint8_t header2; // 帧头2 0xFC
uint8_t cmd_id; // 命令ID 0x41
uint8_t reserved; // 保留, 解决字节对齐问题
uint32_t length; // 数据长度
uint8_t data[CONFIG_PARAM_COUNT*4]; // 数据字段
uint8_t crc; // CRC校验
uint8_t end; // 帧尾 0x2F
uint8_t reserved1; // 保留, 解决字节对齐问题
uint8_t reserved2; // 保留, 解决字节对齐问题
} ConfigCommandPacket_t;
#pragma pack(pop)
DMA发送串口数据时会有4字节对齐问题.
DMA发送串口数据时会有4字节对齐问题. 而且报错无任何信息, 最好的方式是在结构体上都结构体对齐, 否则坑人没得商量.
所以最好是4字节对齐的, 如果实在不好对齐就添加几个无用的字节让它对齐.
大结构体要用静态的或者全局变量.
由于c语言默认将临时变量放在栈上, 栈的空间有限. 当结构体太大的时候, 会导致栈溢出. 所以最好得方法是将大结构体放在静态的变量中.
c
// 拷贝数据包(使用静态分配避免栈溢出)
static ConfigCommandPacket_t config_packet;
CopyFromBuffer((uint8_t*)&config_packet, start_index, packet_length);
使用参数映射表进行参数的收发.
参数的添加和修改, 往往会耗费好多的精力, 为了减少精力消耗, 建议将协议两边的变量名搞一致. 大小写一致.
然后弄个表两边维护, 两边维护的顺序一致. 这样可以避免手写代码带来的顺序问题, 数据类型问题. 方便了很多 .
C语言的结构体赋值可以采用offset 来进行偏移赋值.代码如下
c
typedef struct
{
uint32_t main_software_version; // 1. 软件主版本号
uint32_t secondary_software_version; //2 软件次版本号
uint32_t max_rpm; //3 最大转速
uint32_t idle_rpm; //4 怠速
uint32_t stall_rpm; // 5. 熄火转速
uint32_t max_voltage ;// 6. 最大电压
uint32_t acceleration_curve ;// 7. 加速曲线
uint32_t deceleration_curve;// 8. 减速曲线
uint32_t battery_alarm ;// 9. 电池低压报警值
uint32_t max_temperature ;// 10. 最高温度
uint32_t total_running_time;// 11. 总运行时间
uint32_t restart_at_shutdown;// 12. 熄火重启
......
......
......
} JetSystemConfig_t;
// 参数类型枚举
typedef enum {
PARAM_TYPE_INT32,
PARAM_TYPE_UINT32,
PARAM_TYPE_FLOAT
} ParamType_t;
// 参数映射结构
typedef struct {
uint32_t offset; // 参数在结构体中的偏移量(用于动态访问)
ParamType_t type; // 参数类型
const char* name; // 参数名称
const char* description; // 参数描述
} ParamMap_t;
// 参数对应关系列表
// 注意:此列表定义了参数的发送顺序和接收顺序
const ParamMap_t config_param_map[] = {
{offsetof(JetSystemConfig_t, throttle_accel_curve), PARAM_TYPE_FLOAT, "throttle_accel_curve", "油门加速度曲线系数", },
{offsetof(JetSystemConfig_t, rpm_control_mode), PARAM_TYPE_UINT32, "rpm_control_mode", "转速控制模式", },
{offsetof(JetSystemConfig_t, auto_control_gain), PARAM_TYPE_FLOAT, "auto_control_gain", "自动控制增益", },
{offsetof(JetSystemConfig_t, adaptive_control_factor), PARAM_TYPE_FLOAT, "adaptive_control_factor", "自适应控制因子", },
{offsetof(JetSystemConfig_t, target_rpm), PARAM_TYPE_UINT32, "target_rpm", "目标转速", },
{offsetof(JetSystemConfig_t, max_rpm), PARAM_TYPE_UINT32, "max_rpm", "最大转速", },
{offsetof(JetSystemConfig_t, atmospheric_pressure), PARAM_TYPE_UINT32, "atmospheric_pressure", "环境大气静压力 (Pa)", },
......
......
......
};
// 参数数量
#define CONFIG_PARAM_COUNT (sizeof(config_param_map) / sizeof(ParamMap_t))
//#pragma pack(push, 1)
typedef struct {
uint8_t header1; // 帧头1 0xFE
uint8_t header2; // 帧头2 0xFC
uint8_t cmd_id; // 命令ID 0x41
uint8_t reserved; // 保留, 解决字节对齐问题
uint32_t length; // 数据长度
uint8_t data[CONFIG_PARAM_COUNT*4]; // 数据字段
uint8_t crc; // CRC校验
uint8_t end; // 帧尾 0x2F
uint8_t reserved1; // 保留, 解决字节对齐问题
uint8_t reserved2; // 保留, 解决字节对齐问题
} ConfigCommandPacket_t;
//#pragma pack(pop)
这里还用到了2个技巧. 一个是CONFIG_PARAM_COUNT 数组长度的计算.
另外一个是 动态数组长度的设置. 例如下面的代码, 这里可能要开启C99语法的支持, 太早的C语言估计是不支持这么写的.
c
uint8_t data[CONFIG_PARAM_COUNT*4]; // 数据字段
C语言这边的读取和解析.数据.
c
/**
* @brief 通过USART1一次性向上位机发送当前系统配置所有的参数(g_jet_system.config)
* 防止多次发送导致的数据丢失问题
* @return HAL_StatusTypeDef 发送状态
*/
HAL_StatusTypeDef Comm_ProcessSendAllParametersArray(void) {
//测试数据
// 使用字节数组替代结构体,避免结构体对齐问题
// 帧头(4字节) + 数据包长度帧(4字节) + x个参数*4字节 + CRC(1字节) + 帧尾(1字节)
// 1. 填充帧头和参数总数
param_buffer[0] = 0xFC; // 自定义配置帧头
param_buffer[1] = 0xFC; // 自定义配置帧头
param_buffer[2] = 0xFC; // 自定义配置帧头
param_buffer[3] = 0xFC; // 自定义配置帧头
param_buffer_index = 4;
uint32 package_length = CONFIG_PARAM_COUNT * 4 +10; // 10 = 包括4个帧头+ 4个数据长度+ crc + 尾帧
addUint32ToBuffer(CONFIG_PARAM_COUNT * 4 +2); //数据包长度帧
// 2. 根据参数映射表依次填充所有参数值(每个参数以4字节方式存储,小端序)
for (uint32_t i = 0; i < CONFIG_PARAM_COUNT; i++) {
const ParamMap_t* param = &config_param_map[i];
uint8_t* config_ptr = (uint8_t*)&g_jet_system.config;
if (param->type == PARAM_TYPE_UINT32) {
uint32_t value = *((uint32_t*)(config_ptr + param->offset));
addUint32ToBuffer(value);
//printf("send %s=%d",param->name, value);
} else if (param->type == PARAM_TYPE_FLOAT) {
float value = *((float*)(config_ptr + param->offset));
addFloatToBuffer(value);
//printf("send %s=%f",param->name, value);
} else if (param->type == PARAM_TYPE_INT32) {
int32_t value = *((int32_t*)(config_ptr + param->offset));
addInt32ToBuffer(value);
//printf("send %s=%d",param->name, value);
}
//printf("\r\n");
}
// 3. 计算CRC校验(不包括footer字段)
param_buffer[param_buffer_index++] = JetComm_CalculateCRC(param_buffer, param_buffer_index, 0);
// 4. 设置帧尾
param_buffer[param_buffer_index++] = 0xCF; // 自定义配置帧尾
param_buffer[param_buffer_index++] = 0x00;
param_buffer[param_buffer_index++] = 0x00;
uint32_t length = param_buffer_index;
//param_buffer[4]=param_buffer_index;
// 5. 通过USART1一次性发送整包数据
if (HAL_UART_GetState(&huart1) != HAL_UART_STATE_RESET) {
HAL_StatusTypeDef status = HAL_UART_Transmit(&huart1, param_buffer, length, 1000);
// 发送完成后短暂延时,确保数据发送完整
osDelay(50);
return status;
}
return HAL_ERROR;
}
// 解析配置数据包中的参数
static void ParseConfigParameters(const uint8_t* data_ptr) {
int i = 0;
int* offset = &i;
uint8_t* config_ptr = (uint8_t*)&g_jet_system.config;
// 根据参数映射表依次解析所有参数
for (uint32_t param_idx = 0; param_idx < CONFIG_PARAM_COUNT; param_idx++) {
const ParamMap_t* param = &config_param_map[param_idx];
if (param->type == PARAM_TYPE_UINT32) {
uint32_t value = ReadUint32Data(data_ptr, offset);
*((uint32_t*)(config_ptr + param->offset)) = value;
printf("received %s=>%d",param->name,value);
} else if (param->type == PARAM_TYPE_FLOAT) {
float value = ReadFloatData(data_ptr, offset);
*((float*)(config_ptr + param->offset)) = value;
printf("received %s=>%f",param->name,value);
} else if (param->type == PARAM_TYPE_INT32) {
int32_t value = ReadInt32Data(data_ptr, offset);
*((int32_t*)(config_ptr + param->offset)) = value;
printf("received %s=>%d",param->name,value);
}
printf("\r\n");
}
}
C#这边的读写和配置
csharp
enum ParamType
{
Int32,
UInt32,
Float
}
// 参数映射结构
class ParamMap
{
public uint Offset { get; set; } // 参数在结构体中的偏移量(用于动态访问)
public ParamType Type { get; set; } // 参数类型
public string Name { get; set; } // 参数名称
public string Description { get; set; } // 参数描述
}
// 将下划线命名转换为驼峰命名
private static string UnderscoreToCamelCase(string input)
{
if (string.IsNullOrEmpty(input))
return input;
string[] parts = input.Split('_');
for (int i = 0; i < parts.Length; i++)
{
if (i >= 0 && parts[i].Length > 0)
{
parts[i] = char.ToUpper(parts[i][0]) + parts[i].Substring(1);
}
}
return string.Join("", parts);
}
// 参数对应关系列表
// 注意:此列表定义了参数的发送顺序和接收顺序
private static readonly ParamMap[] ConfigParamMap = {
// 发送顺序 1-58
//new ParamMap { Offset = 0, Type = ParamType.Float, Name = "ignition_pump_voltage", Description = "油泵点火电压" },
new ParamMap { Offset = 0, Type = ParamType.Float, Name = "throttle_accel_curve", Description = "油门加速度曲线系数" },
new ParamMap { Offset = 0, Type = ParamType.UInt32, Name = "rpm_control_mode", Description = "转速控制模式" },
new ParamMap { Offset = 0, Type = ParamType.Float, Name = "auto_control_gain", Description = "自动控制增益" },
new ParamMap { Offset = 0, Type = ParamType.Float, Name = "adaptive_control_factor", Description = "自适应控制因子" },
new ParamMap { Offset = 0, Type = ParamType.UInt32, Name = "target_rpm", Description = "目标转速" },
new ParamMap { Offset = 0, Type = ParamType.UInt32, Name = "max_rpm", Description = "最大转速" },
new ParamMap { Offset = 0, Type = ParamType.UInt32, Name = "atmospheric_pressure", Description = "环境大气静压力 (Pa)" },
....................
....................
....................
};
C#端的发送和解析
csharp
void IEngine.SetParams(ParamsModel paramsModel)
{
List<byte> parameters = new List<byte>(30 * 4 + 5); //
parameters.Add(0xFE);
parameters.Add(0xFC);
parameters.Add(0x41); //设置启动参数
parameters.Add(0x00);//保留位, 解决字节对齐问题
uint length = (uint)(ConfigParamMap.Length * 4 + 10); // + 10 是一个帧头4byte,加一个长度帧4byte, 最后 2byte是crc和尾帧
parameters.AddRange(BitConverter.GetBytes(length)); //数据长度帧
// 使用 ConfigParamMap 反射填充 parameters
foreach (var map in ConfigParamMap)
{
// 将下划线命名转为驼峰命名,并与 paramsModel 的属性名匹配
string propertyName = UnderscoreToCamelCase(map.Name);
var prop = paramsModel.GetType().GetProperty(propertyName);
if (prop == null)
{
throw new Exception($"参数模型中找不到属性: {propertyName}");
}
object value = prop.GetValue(paramsModel);
switch (map.Type)
{
case ParamType.UInt32:
parameters.AddRange(BitConverter.GetBytes(Convert.ToUInt32(value)));
break;
case ParamType.Int32:
parameters.AddRange(BitConverter.GetBytes(Convert.ToInt32(value)));
break;
case ParamType.Float:
parameters.AddRange(BitConverter.GetBytes(Convert.ToSingle(value)));
break;
default:
throw new Exception($"未支持的参数类型: {map.Type}");
}
Console.WriteLine($"设置参数到ecu {propertyName}=>{value}");
}
byte crc = CalculateCRC(parameters.ToArray(), parameters.Count,0);
parameters.Add(crc);
parameters.Add(0x2F);
parameters.Add(0);//保留位, 解决字节对齐问题
parameters.Add(0);//保留位, 解决字节对齐问题
serialPort.Write(parameters.ToArray(), 0, parameters.Count);
}
private void ProcessConfigDataCompleteFrame(byte[] rxBuffer)
{
try
{
// 解析 的配置数据包,按照C语言结构体的字段顺序解析
// 每个字段的偏移量根据C语言结构体的定义确定
BufferReader reader = new BufferReader(rxBuffer);
reader.Skip(8);//跳过帧头 (1字节) 解析参数总数 (1字节)
// 通过 ConfigParamMap 循环读取并反射赋值
foreach (var param in ConfigParamMap)
{
// 将下划线命名转换为驼峰命名,再与 EngineParams 属性名匹配
string propName = UnderscoreToCamelCase(param.Name);
var prop = typeof(ParamsModel).GetProperty(propName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
object val = null;
if (prop == null)
{
val = reader.ReadUInt32();//强制读1个字. 否则顺序对不上号了
throw new Exception($"没有找到属性{propName}");
// continue;
}
switch (param.Type)
{
case ParamType.Float:
val = reader.ReadFloat();
break;
case ParamType.UInt32:
val = reader.ReadUInt32();
break;
case ParamType.Int32:
val = reader.ReadInt32();
break;
}
if (val != null)
{
// 如果目标属性是 uint,而读取到的是 float/int,则转换
if (prop.PropertyType == typeof(uint) && val is float f)
val = (uint)f;
else if (prop.PropertyType == typeof(uint) && val is int i)
val = (uint)i;
prop.SetValue(GV.EngineParams, val);
Console.WriteLine($"串口读取到配置参数 {propName}={val}");
}
}
GV.EngineParams.TriggerConfigRead(); // 触发配置数据接收完成事件
UIMessageTip.ShowOk("参数已读取成功");
}
catch (Exception ex)
{
UIMessageTip.ShowError ($"ProcessCompleteFrame error: {ex.Message}");
}
}
BufferReader类的代码. 方便读取数据.
csharp
/// <summary>
/// 字节缓冲区读取助手类
/// 用于从字节数组读取各种类型的数据,并自动将指针前移
/// </summary>
public class BufferReader
{
private readonly byte[] _buffer;
private int _offset;
/// <summary>
/// 当前偏移量
/// </summary>
public int Offset => _offset;
/// <summary>
/// 剩余可读取的字节数
/// </summary>
public int RemainingBytes => _buffer.Length - _offset;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="buffer">要读取的字节数组</param>
/// <param name="startOffset">起始偏移量,默认为0</param>
public BufferReader(byte[] buffer, int startOffset = 0)
{
_buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
_offset = startOffset;
}
/// <summary>
/// 读取一个字节 (1字节)
/// </summary>
/// <returns>读取到的字节值</returns>
public byte ReadByte()
{
CheckBufferSize(1);
byte value = _buffer[_offset];
_offset += 1;
return value;
}
public byte ReadUInt8()
{
return ReadByte();
}
/// <summary>
/// 读取一个无符号16位整数 (2字节)
/// </summary>
/// <returns>读取到的无符号16位整数值</returns>
public ushort ReadUInt16()
{
CheckBufferSize(2);
ushort value = BitConverter.ToUInt16(_buffer, _offset);
_offset += 2;
return value;
}
/// <summary>
/// 读取一个有符号16位整数 (2字节)
/// </summary>
/// <returns>读取到的有符号16位整数值</returns>
public short ReadInt16()
{
CheckBufferSize(2);
short value = BitConverter.ToInt16(_buffer, _offset);
_offset += 2;
return value;
}
/// <summary>
/// 读取一个无符号32位整数 (4字节)
/// </summary>
/// <returns>读取到的无符号32位整数值</returns>
public uint ReadUInt32()
{
CheckBufferSize(4);
uint value = BitConverter.ToUInt32(_buffer, _offset);
_offset += 4;
return value;
}
/// <summary>
/// 读取一个有符号32位整数 (4字节)
/// </summary>
/// <returns>读取到的有符号32位整数值</returns>
public int ReadInt32()
{
CheckBufferSize(4);
int value = BitConverter.ToInt32(_buffer, _offset);
_offset += 4;
return value;
}
/// <summary>
/// 读取一个单精度浮点数 (4字节)
/// </summary>
/// <returns>读取到的单精度浮点数值</returns>
public float ReadFloat()
{
CheckBufferSize(4);
float value = BitConverter.ToSingle(_buffer, _offset);
_offset += 4;
return value;
}
/// <summary>
/// 读取一个双精度浮点数 (8字节)
/// </summary>
/// <returns>读取到的双精度浮点数值</returns>
public double ReadDouble()
{
CheckBufferSize(8);
double value = BitConverter.ToDouble(_buffer, _offset);
_offset += 8;
return value;
}
/// <summary>
/// 跳过指定数量的字节
/// </summary>
/// <param name="count">要跳过的字节数</param>
public void Skip(int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "跳过的字节数不能为负数");
}
CheckBufferSize(count);
_offset += count;
}
/// <summary>
/// 检查缓冲区大小是否足够
/// </summary>
/// <param name="requiredBytes">需要的字节数</param>
private void CheckBufferSize(int requiredBytes)
{
if (_offset + requiredBytes > _buffer.Length)
{
throw new IndexOutOfRangeException($"缓冲区剩余字节数不足,需要 {requiredBytes} 字节,但只剩下 {RemainingBytes} 字节");
}
}
}
实际上还有更简单的方法 , 直接用定一个结构体加memcpy 来解析,最快最方便. 但是在c#这边就不是很方便了. 还需要注意4字节对齐的问题. 以后C语言这边还是用结构体加memcpy更方便. C#这边还是用map解析的方式更方便.