[经验分享] 多个单片机简单通讯框架

[复制链接]
 楼主| 八层楼 发表于 2025-7-9 08:08 | 显示全部楼层 |阅读模式
一、场景描述
有3个单片机进行通讯,分别为1号,2号,3号。
1号与2号可以通讯,2号与3号可以通讯。

那么如果1号想通过2号,与3号进行通讯。

如何搭建2号的通讯框架,保证1号的通讯能够及时给3号,同时3号也能及时应答给1号。使用串口通讯,协议自定

二、框架搭建
设计思路
为了实现三个单片机之间的可靠中继通信,2号单片机需要设计一个高效的通讯框架。关键在于如何处理串口接收、消息转发和应答机制,同时保证消息的实时性和可靠性。

通信协议设计
首先需要定义一个简单但功能完整的通信协议:

消息格式: [SOF][源地址][目标地址][消息ID][长度][数据][CRC]
  - SOF: 起始字节 (0xAA)
  - 源地址: 发送方ID (1,2,3)
  - 目标地址: 接收方ID (1,2,3)
  - 消息ID: 标识消息类型 (0x01-命令, 0x02-数据, 0x03-应答)
  - 长度: 数据字段长度
  - 数据: 消息内容
  - CRC: 校验字节



2号单片机通讯框架
下面是2号单片机的通讯框架实现,使用状态机处理消息接收,使用双缓冲区实现消息转发:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

// 定义系统常量
#define SOF             0xAA
#define DEVICE_ID_1     0x01
#define DEVICE_ID_2     0x02
#define DEVICE_ID_3     0x03

// 消息类型定义
#define MSG_CMD         0x01
#define MSG_DATA        0x02
#define MSG_ACK         0x03

// 消息缓冲区大小
#define MAX_MSG_LENGTH  64
#define BUFFER_SIZE     5

// 消息结构体
typedef struct {
    uint8_t src_addr;
    uint8_t dst_addr;
    uint8_t msg_id;
    uint8_t length;
    uint8_t data[MAX_MSG_LENGTH];
    uint8_t crc;
} Message;

// 接收状态机枚举
typedef enum {
    RX_STATE_IDLE,
    RX_STATE_SOF,
    RX_STATE_SRC,
    RX_STATE_DST,
    RX_STATE_MSGID,
    RX_STATE_LENGTH,
    RX_STATE_DATA,
    RX_STATE_CRC
} RxState;

// 全局变量
static Message rx_buffer[BUFFER_SIZE];
static Message tx_buffer[BUFFER_SIZE];
static uint8_t rx_head = 0;
static uint8_t rx_tail = 0;
static uint8_t tx_head = 0;
static uint8_t tx_tail = 0;
static RxState rx_state = RX_STATE_IDLE;
static uint8_t rx_index = 0;
static uint8_t crc_calculated = 0;

// 串口初始化函数
void UART1_Init(void) {
    // 配置1号串口(与1号单片机通信)
    // 波特率、中断等配置
}

void UART2_Init(void) {
    // 配置2号串口(与3号单片机通信)
    // 波特率、中断等配置
}

// CRC校验计算
uint8_t calculate_crc(uint8_t *data, uint8_t length) {
    uint8_t crc = 0;
    for (uint8_t i = 0; i < length; i++) {
        crc ^= data[i];
    }
    return crc;
}

// 发送消息函数
void send_message(Message *msg, uint8_t uart_id) {
    uint8_t buffer[MAX_MSG_LENGTH + 6];
    uint8_t index = 0;

    buffer[index++] = SOF;
    buffer[index++] = msg->src_addr;
    buffer[index++] = msg->dst_addr;
    buffer[index++] = msg->msg_id;
    buffer[index++] = msg->length;

    for (uint8_t i = 0; i < msg->length; i++) {
        buffer[index++] = msg->data[i];
    }

    buffer[index++] = calculate_crc(buffer, index);

    // 根据uart_id选择发送串口
    if (uart_id == 1) {
        // 通过UART1发送buffer
    } else {
        // 通过UART2发送buffer
    }
}

// 1号串口接收中断处理函数
void UART1_RX_IRQHandler(void) {
    uint8_t data = UART1_ReadByte();

    switch (rx_state) {
        case RX_STATE_IDLE:
            if (data == SOF) {
                rx_state = RX_STATE_SOF;
                crc_calculated = data;
            }
            break;

        case RX_STATE_SOF:
            rx_buffer[rx_head].src_addr = data;
            rx_state = RX_STATE_SRC;
            crc_calculated ^= data;
            break;

        case RX_STATE_SRC:
            rx_buffer[rx_head].dst_addr = data;
            rx_state = RX_STATE_DST;
            crc_calculated ^= data;
            break;

        case RX_STATE_DST:
            rx_buffer[rx_head].msg_id = data;
            rx_state = RX_STATE_MSGID;
            crc_calculated ^= data;
            break;

        case RX_STATE_MSGID:
            rx_buffer[rx_head].length = data;
            rx_state = RX_STATE_LENGTH;
            crc_calculated ^= data;
            rx_index = 0;
            break;

        case RX_STATE_LENGTH:
            rx_buffer[rx_head].data[rx_index++] = data;
            crc_calculated ^= data;
            if (rx_index >= rx_buffer[rx_head].length) {
                rx_state = RX_STATE_CRC;
            }
            break;

        case RX_STATE_CRC:
            if (data == crc_calculated) {
                // 消息接收成功,加入接收缓冲区
                rx_head = (rx_head + 1) % BUFFER_SIZE;
            }
            rx_state = RX_STATE_IDLE;
            break;
    }
}

// 3号串口接收中断处理函数(与UART1_RX_IRQHandler类似)
void UART2_RX_IRQHandler(void) {
    // 实现类似UART1_RX_IRQHandler的逻辑
    // 将接收到的消息存入rx_buffer
}

// 主循环中的消息处理函数
void process_messages(void) {
    Message msg;

    // 处理从1号单片机接收的消息
    while (rx_tail != rx_head) {
        msg = rx_buffer[rx_tail];

        // 如果消息是给3号的,转发到3号
        if (msg.dst_addr == DEVICE_ID_3) {
            // 修改源地址为2号
            msg.src_addr = DEVICE_ID_2;
            send_message(&msg, 2);  // 通过UART2发送到3号
        }
        // 如果消息是给1号的(来自3号的应答),转发到1号
        else if (msg.dst_addr == DEVICE_ID_1) {
            // 修改源地址为2号
            msg.src_addr = DEVICE_ID_2;
            send_message(&msg, 1);  // 通过UART1发送到1号
        }

        rx_tail = (rx_tail + 1) % BUFFER_SIZE;
    }
}

// 主函数
int main(void) {
    UART1_Init();
    UART2_Init();

    // 启用中断
    UART1_EnableIRQ();
    UART2_EnableIRQ();

    while (1) {
        // 处理消息转发
        process_messages();

        // 其他任务
        // ...
    }
}



框架优化建议
优先级机制:为紧急消息设置高优先级,优先处理和转发。

流量控制:当缓冲区满时,发送忙信号给发送方,防止消息丢失。

超时重传:对重要消息实现超时重传机制,确保消息可靠传输。

状态监控:添加通信状态监控,统计消息收发成功率,便于故障排查。

中断处理优化:尽量减少中断处理时间,关键数据复制到缓冲区后尽快退出中断。

通过以上设计,2号单片机可以高效地实现1号和3号之间的消息中继,同时保证通信的实时性和可靠性。

三、2号单片机的通讯框架如何处理消息丢失和重传?
消息丢失与重传机制设计
为了处理消息丢失和实现可靠重传,2号单片机的通讯框架需要增加确认机制、超时处理和消息缓存功能。以下是具体实现方案:

改进的通信协议
在原有协议基础上增加序列号和确认应答:

消息格式: [SOF][源地址][目标地址][消息ID][序列号][长度][数据][CRC]
  - 序列号: 8位递增数字(0-255),用于标识消息
  - 确认应答(ACK)格式: [SOF][源地址][目标地址][MSG_ACK][序列号][0][CRC]



重传机制实现
以下是改进后的2号单片机通讯框架代码,增加了消息缓存和重传机制:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// 定义系统常量
#define SOF             0xAA
#define DEVICE_ID_1     0x01
#define DEVICE_ID_2     0x02
#define DEVICE_ID_3     0x03

// 消息类型定义
#define MSG_CMD         0x01
#define MSG_DATA        0x02
#define MSG_ACK         0x03

// 消息缓冲区大小
#define MAX_MSG_LENGTH  64
#define TX_BUFFER_SIZE  10
#define RX_BUFFER_SIZE  10
#define MAX_RETRIES     3
#define RETRY_TIMEOUT   100  // 毫秒

// 消息结构体
typedef struct {
    uint8_t src_addr;
    uint8_t dst_addr;
    uint8_t msg_id;
    uint8_t seq_num;
    uint8_t length;
    uint8_t data[MAX_MSG_LENGTH];
    uint8_t crc;
    uint32_t timestamp;  // 发送时间戳
    uint8_t retries;     // 重试次数
    bool awaiting_ack;   // 是否等待确认
} Message;

// 接收状态机枚举
typedef enum {
    RX_STATE_IDLE,
    RX_STATE_SOF,
    RX_STATE_SRC,
    RX_STATE_DST,
    RX_STATE_MSGID,
    RX_STATE_SEQ,
    RX_STATE_LENGTH,
    RX_STATE_DATA,
    RX_STATE_CRC
} RxState;

// 全局变量
static Message tx_buffer[TX_BUFFER_SIZE];  // 发送缓冲区(用于重传)
static Message rx_buffer[RX_BUFFER_SIZE];  // 接收缓冲区
static uint8_t tx_head = 0;
static uint8_t tx_tail = 0;
static uint8_t rx_head = 0;
static uint8_t rx_tail = 0;
static RxState rx_state = RX_STATE_IDLE;
static uint8_t rx_index = 0;
static uint8_t crc_calculated = 0;
static uint8_t next_seq_num = 0;  // 下一个发送的序列号

// 串口初始化函数
void UART1_Init(void) {
    // 配置1号串口(与1号单片机通信)
    // 波特率、中断等配置
}

void UART2_Init(void) {
    // 配置2号串口(与3号单片机通信)
    // 波特率、中断等配置
}

// CRC校验计算
uint8_t calculate_crc(uint8_t *data, uint8_t length) {
    uint8_t crc = 0;
    for (uint8_t i = 0; i < length; i++) {
        crc ^= data[i];
    }
    return crc;
}

// 发送消息函数
void send_message(Message *msg, uint8_t uart_id, bool store_for_retry) {
    uint8_t buffer[MAX_MSG_LENGTH + 7];
    uint8_t index = 0;

    buffer[index++] = SOF;
    buffer[index++] = msg->src_addr;
    buffer[index++] = msg->dst_addr;
    buffer[index++] = msg->msg_id;
    buffer[index++] = msg->seq_num;
    buffer[index++] = msg->length;

    for (uint8_t i = 0; i < msg->length; i++) {
        buffer[index++] = msg->data[i];
    }

    buffer[index++] = calculate_crc(buffer, index);

    // 根据uart_id选择发送串口
    if (uart_id == 1) {
        // 通过UART1发送buffer
    } else {
        // 通过UART2发送buffer
    }

    // 如果需要存储用于重传
    if (store_for_retry && msg->msg_id != MSG_ACK) {
        // 复制消息到发送缓冲区
        memcpy(&tx_buffer[tx_head], msg, sizeof(Message));
        tx_buffer[tx_head].timestamp = get_current_time();  // 获取当前时间
        tx_buffer[tx_head].retries = 0;
        tx_buffer[tx_head].awaiting_ack = true;
        tx_head = (tx_head + 1) % TX_BUFFER_SIZE;
    }
}

// 1号串口接收中断处理函数
void UART1_RX_IRQHandler(void) {
    uint8_t data = UART1_ReadByte();

    switch (rx_state) {
        case RX_STATE_IDLE:
            if (data == SOF) {
                rx_state = RX_STATE_SOF;
                crc_calculated = data;
            }
            break;

        // 其他状态处理...(与之前类似)

        case RX_STATE_CRC:
            if (data == crc_calculated) {
                // 消息接收成功,加入接收缓冲区
                if (rx_buffer[rx_head].msg_id == MSG_ACK) {
                    // 处理确认应答,标记对应消息已确认
                    process_ack(&rx_buffer[rx_head]);
                } else {
                    // 普通消息,转发并发送本地ACK
                    forward_message(&rx_buffer[rx_head]);
                    send_ack(rx_buffer[rx_head].src_addr, rx_buffer[rx_head].seq_num);
                }
                rx_head = (rx_head + 1) % RX_BUFFER_SIZE;
            }
            rx_state = RX_STATE_IDLE;
            break;
    }
}

// 3号串口接收中断处理函数
void UART2_RX_IRQHandler(void) {
    // 与UART1_RX_IRQHandler类似,处理来自3号的消息
}

// 处理确认应答
void process_ack(Message *ack) {
    uint8_t index = tx_tail;

    while (index != tx_head) {
        if (tx_buffer[index].awaiting_ack &&
            tx_buffer[index].seq_num == ack->seq_num &&
            tx_buffer[index].dst_addr == ack->src_addr) {
            // 标记消息已确认
            tx_buffer[index].awaiting_ack = false;
            break;
        }
        index = (index + 1) % TX_BUFFER_SIZE;
    }
}

// 转发消息
void forward_message(Message *msg) {
    Message forward_msg;
    memcpy(&forward_msg, msg, sizeof(Message));

    if (msg->dst_addr == DEVICE_ID_3) {
        // 转发到3号
        forward_msg.src_addr = DEVICE_ID_2;
        send_message(&forward_msg, 2, true);
    } else if (msg->dst_addr == DEVICE_ID_1) {
        // 转发到1号
        forward_msg.src_addr = DEVICE_ID_2;
        send_message(&forward_msg, 1, true);
    }
}

// 发送确认应答
void send_ack(uint8_t dst_addr, uint8_t seq_num) {
    Message ack_msg;
    ack_msg.src_addr = DEVICE_ID_2;
    ack_msg.dst_addr = dst_addr;
    ack_msg.msg_id = MSG_ACK;
    ack_msg.seq_num = seq_num;
    ack_msg.length = 0;

    if (dst_addr == DEVICE_ID_1) {
        send_message(&ack_msg, 1, false);
    } else {
        send_message(&ack_msg, 2, false);
    }
}

// 检查并重传超时消息
void check_and_retransmit(void) {
    uint32_t current_time = get_current_time();
    uint8_t index = tx_tail;

    while (index != tx_head) {
        if (tx_buffer[index].awaiting_ack &&
            (current_time - tx_buffer[index].timestamp > RETRY_TIMEOUT)) {

            // 检查重试次数
            if (tx_buffer[index].retries >= MAX_RETRIES) {
                // 达到最大重试次数,处理失败
                handle_transmission_failure(&tx_buffer[index]);
                tx_buffer[index].awaiting_ack = false;
            } else {
                // 重传消息
                tx_buffer[index].retries++;
                tx_buffer[index].timestamp = current_time;

                if (tx_buffer[index].dst_addr == DEVICE_ID_1) {
                    send_message(&tx_buffer[index], 1, false);
                } else {
                    send_message(&tx_buffer[index], 2, false);
                }
            }
        }
        index = (index + 1) % TX_BUFFER_SIZE;
    }

    // 清理已确认的消息
    while (tx_tail != tx_head && !tx_buffer[tx_tail].awaiting_ack) {
        tx_tail = (tx_tail + 1) % TX_BUFFER_SIZE;
    }
}

// 处理传输失败
void handle_transmission_failure(Message *msg) {
    // 可以实现错误日志记录或通知上层应用
}

// 获取当前时间(伪代码,需根据实际硬件实现)
uint32_t get_current_time(void) {
    // 返回系统时间(毫秒)
    return 0;
}

// 主循环中的消息处理函数
void process_messages(void) {
    // 检查并重传超时消息
    check_and_retransmit();

    // 处理接收缓冲区中的消息
    while (rx_tail != rx_head) {
        // 处理消息...
        rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE;
    }
}

// 主函数
int main(void) {
    UART1_Init();
    UART2_Init();

    // 启用中断
    UART1_EnableIRQ();
    UART2_EnableIRQ();

    while (1) {
        // 处理消息转发和重传
        process_messages();

        // 其他任务
        // ...
    }
}



关键机制说明
序列号管理:

每个消息分配唯一序列号(0-255循环)
用于识别重复消息和匹配ACK应答
确认应答机制:

接收方收到消息后发送ACK应答
ACK包含原始消息的序列号
发送方维护待确认消息列表
超时重传:

设置合理的超时时间(RETRY_TIMEOUT)
超过时间未收到ACK则重传
限制最大重试次数(MAX_RETRIES)
消息缓存:

使用环形缓冲区存储待确认消息
消息包含发送时间戳和重试次数
优化建议
滑动窗口协议:

扩展当前实现,支持多消息并发发送
增加窗口大小参数,提高吞吐量
动态超时调整:

根据网络状况动态调整超时时间
实现往返时间(RTT)测量
流量控制:

当发送缓冲区满时拒绝接收新消息
实现基于窗口的流量控制机制
错误恢复:

实现序列号回滚机制
处理序列号溢出情况
通过以上机制,2号单片机可以有效处理消息丢失问题,确保1号和3号之间的通信可靠性。



————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_43582136/article/details/149071161

ingramward 发表于 2025-8-7 10:32 | 显示全部楼层
定义错误处理机制,包括超时重传、错误检测和恢复策略。
wengh2016 发表于 2025-8-8 11:57 | 显示全部楼层
短距离、低速率场景可选择串口通信,而长距离、抗干扰要求高的场景可选择RS-485
mikewalpole 发表于 2025-8-8 16:39 | 显示全部楼层
高速、点对点        SPI              
wilhelmina2 发表于 2025-8-11 16:39 | 显示全部楼层
如果单片机数量较多,可以为每个单片机分配一个唯一的地址。在通讯时,发送方需要指定目标单片机的地址。
earlmax 发表于 2025-8-12 15:36 | 显示全部楼层
UART最常用的串行通信方式之一,适合点对点通信。支持全双工通信
uptown 发表于 2025-8-12 16:08 | 显示全部楼层
多个单片机之间的简单通讯框架设计需兼顾成本、可靠性和开发效率
i1mcu 发表于 2025-8-12 17:13 | 显示全部楼层
一个主控单片机和两个从属单片机,主控单片机定期向从属单片机发送命令,并接收它们的状态报告。
robincotton 发表于 2025-8-12 19:48 | 显示全部楼层
为了提高系统的实时性和效率,可以使用中断机制来处理通讯事件,如接收数据完成或发送数据完成。
sesefadou 发表于 2025-8-12 20:35 | 显示全部楼层
对关键通讯任务 启用硬件看门狗,防止死锁。
cashrwood 发表于 2025-8-12 21:18 | 显示全部楼层
常见方案包括串口(UART)、I2C、SPI、CAN及无线通讯
beacherblack 发表于 2025-8-12 22:07 | 显示全部楼层
串口适用于短距离、低成本且对实时性要求不高的场景
sheflynn 发表于 2025-8-12 22:36 | 显示全部楼层
使用UART进行多个单片机之间的简单通讯
pmp 发表于 2025-8-14 13:29 | 显示全部楼层
通讯协议中包含错误检测机制              
chenci2013 发表于 2025-8-14 15:09 | 显示全部楼层
串口通信是一种简单且常用的单片机通信方式,适用于短距离、低速率的数据传输。
wangdezhi 发表于 2025-8-14 18:44 | 显示全部楼层
如果是双向通信,则每一对单片机都需要独立的TX/RX线路,或者可以考虑使用RS485等差分信号标准来扩展距离和支持更多的设备。
primojones 发表于 2025-8-14 22:33 | 显示全部楼层
I2C可以多传感器、低速                      
ulystronglll 发表于 2025-8-14 22:57 | 显示全部楼层
选择合适的通讯接口,如UART、I2C、SPI等。
10299823 发表于 2025-8-15 20:58 | 显示全部楼层
推荐以UART+自定义协议入门,因其实现简单且兼容性强
jimmhu 发表于 2025-8-15 22:10 | 显示全部楼层
对于实时性要求高的应用,减少延迟尤为重要。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

119

主题

4344

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部