C语言单片机与C#上位机之间传递大量参数比较好的实践方案

几个关键点.

结构体要注意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解析的方式更方便.

相关推荐
代码游侠2 小时前
复习——Linux 系统编程
linux·运维·c语言·学习·算法
发疯幼稚鬼2 小时前
希尔排序与堆排序
c语言·数据结构·算法·排序算法
猪八戒1.03 小时前
【梅花】2.工程模板的搭建
单片机·嵌入式硬件
清风6666663 小时前
基于单片机的井盖安全监测与报警上位机监测系统设计
单片机·嵌入式硬件·安全·毕业设计·课程设计·期末大作业
清风6666663 小时前
基于单片机的多功能LCD音乐播放器设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
国科安芯3 小时前
车规级芯片的AECQ100规范及详细测试项目介绍——以ASM1042型CAN FD收发器芯片为例
单片机·嵌入式硬件·架构·安全威胁分析·安全性测试
辜月廿七3 小时前
C#字符串相关知识
c#
Coding_Doggy3 小时前
重装系统C盘格式化,MYSQL恢复
c语言·mysql·adb
历程里程碑3 小时前
C++ 8:list容器详解与实战指南
c语言·开发语言·数据库·c++·windows·笔记·list