4、CAN通信实战:CAN报文收发、波特率配置、CAN滤波器设置、CAN错误处理

好,咱们进入正题。UWB车钥匙最终要跟车身通信,靠的就是CAN总线。这一章我带你手把手把CAN通信跑起来。说白了,就是让UWB定位模块和BCM(车身控制器)能互相听懂对方在说什么。

4.1 CAN报文收发——别光看,动手发一条

CAN报文的结构其实不复杂。标准帧11位ID,扩展帧29位ID。咱们UWB车钥匙场景里,一般用标准帧就够了。我习惯把ID规划成三层:功能码+节点地址+消息序号。

举个例子,UWB模块上报位置信息,我常用0x3A0这个ID。数据段放8个字节:前4个字节是X坐标,后4个字节是Y坐标。嗯,这里要注意,CAN的数据段是小端模式,低字节在前。

核心要点:发送报文时,一定要确认总线空闲。我见过新手直接往发送寄存器里写数据,结果总线仲裁失败,报文死活发不出去。
// 发送一帧CAN数据(基于STM32 HAL库)
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t TxMailbox;

TxHeader.StdId = 0x3A0;      // 标准ID
TxHeader.ExtId = 0;          // 扩展ID不用
TxHeader.IDE = CAN_ID_STD;   // 标准帧
TxHeader.RTR = CAN_RTR_DATA; // 数据帧
TxHeader.DLC = 8;            // 数据长度

// 检查是否有空闲邮箱
if (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) > 0) {
    HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox);
    // 发送成功,TxMailbox返回使用的邮箱号
}

接收呢?我建议用中断或者FIFO。轮询方式在UWB这种实时性要求高的场景下,容易丢帧。我在项目里吃过这个亏——UWB定位数据每10ms来一次,轮询稍微慢一点,缓冲区就溢出了。

// CAN接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef RxHeader;
    uint8_t RxData[8];
    
    if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK) {
        // 判断ID,0x3A0是UWB位置数据
        if (RxHeader.StdId == 0x3A0) {
            // 提取坐标
            uint32_t x_pos = RxData[0] | (RxData[1] << 8) | (RxData[2] << 16) | (RxData[3] << 24);
            uint32_t y_pos = RxData[4] | (RxData[5] << 8) | (RxData[6] << 16) | (RxData[7] << 24);
            // 更新位置信息
            uwb_position_update(x_pos, y_pos);
        }
    }
}

4.2 波特率配置——别小看这个参数

波特率配置不对,总线直接瘫痪。你想想看,车上那么多ECU,每个都得用同一个速率说话。UWB车钥匙模块一般用500kbps,这是汽车行业最常用的速率之一。

波特率怎么算?说白了就是CAN时钟频率除以分频系数,再除以(同步段+传播段+相位缓冲段1+相位缓冲段2)。我习惯用STM32的CAN外设举例:

参数 说明 推荐值(500kbps)
CAN时钟 APB1时钟频率 42MHz
分频系数 BRP分频 6
同步段 固定为1个Tq 1
传播段 补偿物理延迟 2
相位缓冲段1 采样点位置 7
相位缓冲段2 采样点后 4
采样点 (同步段+传播段+PS1)/总Tq 约71.4%
我的经验:采样点设置在70%~80%之间最稳。我曾经把采样点设到85%,结果在高温测试时总线疯狂报错。后来查了半天,发现是线束太长导致信号延迟,采样点太靠后正好采到了信号跳变沿。
// STM32 CAN波特率配置示例
hcan.Instance = CAN1;
hcan.Init.Prescaler = 6;           // 分频系数
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;  // 同步跳转宽度
hcan.Init.TimeSeg1 = CAN_BS1_9TQ;  // 传播段+相位缓冲段1 = 2+7=9
hcan.Init.TimeSeg2 = CAN_BS2_4TQ;  // 相位缓冲段2 = 4
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE;  // 自动重发,我建议开启
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
HAL_CAN_Init(&hcan);

4.3 CAN滤波器设置——别让UWB模块收到无关消息

CAN总线上跑着几百条消息,发动机转速、车门状态、空调温度...你想想看,UWB模块要是每条都处理,CPU早就累趴了。滤波器就是干这个的——只让UWB关心的消息进来。

我一般用掩码模式。说白了就是设置一个ID模板和一个掩码,掩码为1的位必须匹配,为0的位可以忽略。

实战配置:UWB车钥匙模块只关心0x3A0~0x3AF这16个ID。掩码设置成0x7F0,ID模板设成0x3A0。这样0x3A0到0x3AF都能通过,其他ID全部过滤掉。
// CAN滤波器配置 - 只接收0x3A0~0x3AF
CAN_FilterTypeDef sFilterConfig;

sFilterConfig.FilterBank = 0;                    // 使用滤波器组0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32位
sFilterConfig.FilterIdHigh = (0x3A0 << 5) >> 8;  // ID高16位
sFilterConfig.FilterIdLow = (0x3A0 << 5) | CAN_ID_STD; // ID低16位+IDE位
sFilterConfig.FilterMaskIdHigh = (0x7F0 << 5) >> 8; // 掩码高16位
sFilterConfig.FilterMaskIdLow = (0x7F0 << 5) | CAN_ID_STD; // 掩码低16位
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 关联到FIFO0
sFilterConfig.FilterActivation = ENABLE;

HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);

嗯,这里有个坑。滤波器配置时,ID和掩码的位对齐很容易搞错。我建议你先把ID左移5位(因为标准帧ID占11位,左移后对齐到16位的高11位),然后再拆分高低字节。我曾经因为位没对齐,滤波器死活不工作,排查了一整天。

4.4 CAN错误处理——别等出事了再后悔

CAN总线错误分五种:位错误、填充错误、CRC错误、格式错误、应答错误。UWB车钥匙模块在车上跑,最容易遇到的是位错误和应答错误。

为什么会这样?你想想看,UWB模块可能装在车门把手附近,线束长,干扰大。信号质量不好,位错误就来了。应答错误呢?一般是目标节点没收到消息或者挂了。

警告:CAN控制器有错误计数器。发送错误计数超过255,节点会自动进入Bus Off状态,彻底断开总线。这时候UWB车钥匙就彻底失联了。我见过一个项目,UWB模块频繁重发导致错误计数飙升,最后总线被踢出,车主打不开车门。

我的做法是:监控错误状态,主动干预。

// CAN错误处理示例
void CAN_ErrorHandler(CAN_HandleTypeDef *hcan) {
    HAL_CAN_StateTypeDef canState = HAL_CAN_GetState(hcan);
    uint32_t errorCode = HAL_CAN_GetError(hcan);
    
    // 检查是否进入Bus Off
    if (canState == HAL_CAN_STATE_BUS_OFF) {
        // 主动恢复,不要等自动恢复
        HAL_CAN_Stop(hcan);
        HAL_Delay(10);  // 等待总线安静
        HAL_CAN_Start(hcan);
        
        // 记录错误日志
        can_error_log("CAN Bus Off detected, manually recovered");
    }
    
    // 检查错误计数器
    if (errorCode & HAL_CAN_ERROR_TX_LAST) {
        // 发送错误,可能是总线短路或干扰
        can_error_log("CAN TX error, check bus wiring");
    }
    
    if (errorCode & HAL_CAN_ERROR_ACK) {
        // 应答错误,目标节点可能不在线
        can_error_log("CAN ACK error, target node offline");
    }
}
避坑指南:我曾经在UWB模块上遇到过一个问题——车辆熄火后,CAN总线进入休眠模式,UWB模块还在发消息,结果应答错误不断累积。后来我加了一个总线状态检测:如果连续10次发送没有应答,就停止发送并进入低功耗模式,等总线唤醒信号来了再恢复。

还有一个实用技巧:在UWB模块的CAN驱动里加一个错误统计任务。每秒钟统计一次错误计数,如果增长太快,就主动降低发送频率,或者切换到备用通信通道。说白了,就是别让错误计数器爆掉。

好了,这一章的内容就这些。CAN通信看起来简单,但实际项目中坑不少。你把这些代码和配置吃透了,UWB车钥匙的CAN通信部分基本就稳了。下一章咱们聊聊UWB定位数据怎么通过CAN总线传给BCM,以及怎么保证实时性。