PFC与LLC的SCI通信协议解析

PFC与LLC的SCI通信协议解析

在复杂的隔离型数字电源系统中,前级功率因数校正(PFC)与后级谐振变换器(LLC)通常由两颗独立的 DSP 控制器分别管理。为了实现系统的整机协同与保护联锁,两者之间必须建立高效且可靠的隔离通信机制。

本章将详细为您解析 PFC 与 LLC 之间的 SCI(串行通信接口)通信协议,从帧结构定义、宏观与微观时序控制,到中断收发逻辑的底层实现进行深度剖析。


隔离通信系统概述

在系统物理架构上,PFC与LLC控制器独立工作,其中PFC控制器位于电源初级侧,LLC控制器位于电源次级侧。
由于拓扑的级联关系,LLC工作基于PFC的运行状态,当PFC出现故障或由于PFC输入电压(电网电压)不满足合理范围时,LLC必须禁止输出。同时,通常电源模块会将输入电压电流等参数上报上位机,而实际负责与外部上位机通信的往往是次级侧的LLC控制器。因此,正常运行过程中,PFC控制器需要将自己的工作状态内容以及相关电压电流参数跨越隔离带传输至LLC控制器,即两者之间需建立隔离信息交互的通信电路。

为了实现这一目标,模块设计了隔离型SCI串行通信方式实现PFC控制器与LLC控制器之间的信息交互。
在通信主从关系上,定义LLC控制器为通信主机,PFC控制器为从机。从机(PFC)绝不主动发送信息,只有在接收到主机(LLC)的明确命令后,才回传对应的数据内容,避免了总线冲突。
考虑到隔离光耦的传输延迟以及对抗干扰能力的要求,配置SCI通信的波特率为稳定的 4800bit/秒。


SCI 通信帧格式设计

信息交互以固定的数据帧格式组成,为了兼顾传输效率与数据完整性,定义通信内容一帧由6个字节组成,其结构如下图所示:

帧结构

  • 帧头(Byte 0):其中Byte0为帧头字节,用以表示该数据帧的内容命令。为了在硬件底层直接区分帧头与普通数据,该字节传输时利用了 DSP 串口的多机通信模式,将其第九位设置为1。控制器在发送一帧数据的首字节地址时,使用TXWAKE寄存器添加第9位,用以代表这是一个全新的帧头。
  • 有效数据载荷(Byte 1 ~ Byte 4):Byte1~Byte4为该帧的有效物理数据或状态信息。
  • 校验和(Byte 5):Byte5为校验和字节,它是Byte0~Byte4五个数据累加和后取低八位的值,该字节用以校验数据在传输过程中无任何电磁干扰造成的差错。

校验逻辑: 任何发送方在组帧时,将Byte0~Byte4求和后作为Byte5数据发送;任何接收方在接收完一个完整帧后对该帧进行求校验和,当且仅当计算出的校验和与接收到的该帧Byte5相同时,才认为其为有效数据帧,否则直接将其丢弃,防止错误数据引发危险动作。

通信命令字与载荷定义

设计中,定义LLC(主机)向PFC发送两种数据命令,其帧头分别为 CMD1: 0x11CMD2: 0x33
因为主机的主要目的是“索要”数据,所以在LLC向PFC发送的这两种结构帧中,Byte1~Byte4有效数据均为0。

定义PFC(从机)在接收完对应的命令帧后,回复的数据内容格式如下:

  • 回复 CMD1 帧数据(状态巡检):Byte0为PFC故障信息高位,Byte1为PFC故障信息低位,Byte2为PFC状态信息高位,Byte3为PFC状态信息低位。
  • 回复 CMD2 帧数据(参数巡检):Byte0为输入电压有效值高位,Byte1为输入电压有效值低位,Byte2为输入电流有效值高位,Byte3为输入电流有效值低位。

宏观与微观通信时序

为了避免频繁的中断打断高频发波控制,SCI 的通信时序被精细地分为“宏观轮询间隔”与“微观字节间隔”。

宏观轮询时序 (Frame Timing)

PFC与LLC控制器的宏观通信时间间隔设定为 100mS。如下图所示为PFC与LLC通信帧的时间时序图:

通信时间时序图

  • 首先,主机LLC向PFC发送命令帧CMD1(数据空),PFC在检测到CMD1帧并校验数据无错误后,立即回复CMD1帧(含PFC状态数据内容)。
  • 主机LLC在发送完CMD1后,会静静等待100mS的间隔,然后再向PFC发送命令帧CMD2(数据空)。PFC在检测到CMD2帧并校验数据无错误后,回复CMD2帧(含PFC电压电流数据内容)。
  • 主机LLC再等待100mS后重新发送命令帧CMD1(数据空),系统以此 200mS 为完整周期的节拍循环发送。

微观字节时序 (Byte Timing)

定义的每一帧数据由6个字节组成,由于 SCI 为单字节发送外设,即需要连续调用 6 个发送数据操作才能完成一帧数据发送。
考虑到波特率较慢,为了不让 CPU 死等串口发送完成,定义了 SCI 一帧中每个字节的微观通信时间间隔,如下图所示:

字节发送时序

以LLC控制器向PFC控制器发送一帧数据为例,低字节Byte0优先被推送发送,随后 CPU 离开;等待 5mS 的定时器中断到来时再发送第 2 字节,以此 5ms 的时间间隔为节拍,从容地完成整帧数据发送。
LLC和PFC都可以巧妙地于 5mS 的慢速定时器中断中进行单字节发送操作,实现了极为高效的时分复用。
对于接收端而言,PFC在接收到Byte0字节(含地址位,第九位为1)后,开始组建帧操作;当累计接收满6个字节数据后,在数据校验无问题后才将6字节组建为一帧合法数据。


状态机驱动的发送事件逻辑

LLC 主机发送事件 (SciCom)

LLC 为主机,掌控着通信的绝对节奏,其程序流程图如下图所示:

LLC发送程序流程图

SciCom 函数中,LLC 于每 100mS 时间间隔(即 5ms 中断的计数器累加大于 20 次)主动向 PFC 发起命令帧。当计数器大于 20,计数器清零,程序将需要发送的命令字装载进发送数组(此时载荷为空),并置位发送标志位。
当底层轮询检测到该发送标志为1时,进入 FrameSent() 状态机函数,将一帧数据(6字节)以每 5mS 发送 1 字节的时间间隔有条不紊地送出。

PFC 从机发送事件 (SciCom)

PFC 为从机,绝不主动发送数据,只有在接收完整一帧来自 LLC 控制器的合法命令后才回复数据帧。其程序流程图如下所示:

PFC发送程序流程图

PFC 在串口接收中断中接收完 LLC 命令帧并校验正确后,置位自身的发送标志位。在主循环或 5ms 调度中检测到该标志位为1时,说明 PFC 需要回复对应命令帧的数据。此时,PFC 迅速将需要回复的电压/电流/状态数据装载进发送数组,进入 FrameSent() 函数,同样将一帧数据(6字节)以每 5mS 发送 1 字节的时间间隔安全送出。


硬件级中断接收与校验事件

为了保证接收的实时性,数据的捕获由底层 SCI 硬件接收中断完成。

PFC 中断接收 (ISR_SCI)

PFC在接收完 1 个完整字节后,硬件自动触发并进入 SCI 接收中断函数 __interrupt void ISR_SCI(void)。其程序流程图如下所示:

PFC接收中断

  1. 寻址与同步:进入接收中断后,首要任务是判断接收字节数据的第九位是否为1(以此判断是否为一帧数据的首字节/帧头)。若接收字节为首字节,则立即存储首字节命令,并将状态机 SciReg.cmd 设置至对应值,同时将该数据保存至 SciReg.RxFrame 帧数组的第 0 位。
  2. 数据填入:随后再次接收到普通数据时(第九位为0),依次顺序保存接收数据至 SciReg.RxFrame 帧数组中。
  3. 校验与动作:当累计接收到 6 个字节数据后,CPU 立即对 SciReg.RxFrame 数组中的 Byte0~Byte4 进行求和,并与接收到的 Byte5 字节进行对比,以校验接收数据准确性。
    • 若校验和不对,说明遭遇干扰,数据立刻丢弃,防止误动作。
    • 若校验数据准确无误,则置位发送标志位准备回传。由于PFC接收来自于LLC控制器的命令帧的数据载荷为空,因此无需进行额外的应用层数据转存操作。

LLC 中断接收

LLC 的中断接收程序流程图与 PFC 接收中断在底层架构上完全相同,均依靠第九位寻址和累加和校验。

不同之处在于应用层:LLC 接收来自于 PFC 的回复帧中含有极其重要的 PFC 运行状态、母线电压等信息。因此,在硬件帧校验准确通过后,LLC 必须立刻将数据帧中的 Byte1~Byte4 有效数据,按位解包并转存至对应的系统变量结构体中,供 StateM 状态机判断联锁保护条件使用。

PFC的SCI通信程序深度解析

PFC的SCI通信程序深度解析

在上一章中,我们详细探讨了 LLC(主机)与 PFC(从机)之间的 SCI 隔离通信协议架构及 LLC 侧的运行逻辑。本章将把视角切换到前级——PFC 侧(从机)的通信程序实现

作为从机,PFC 控制器绝不主动发起通信,而是时刻“监听”总线。只有在准确接收并校验来自 LLC 的寻址命令帧后,才会将自身的电压、电流、故障标志及状态机等核心数据打包回传。这种“一问一答”的轮询机制,既保证了时序的确定性,又最大限度地避免了总线冲突。

下面我们将按照“接收中断 -> 数据解析 -> 数据组帧 -> 延时发送”的完整数据流行进路线,对 PFC 侧的 C 语言源代码进行逐行剖析。


通信宏定义与全局变量声明

在程序的最开始,定义了通信过程中的各种命令字宏以及核心的状态寄存器变量。

1
2
3
4
5
/********************MESSAGE**********************/
#define MESSAGE1 0x0001
#define MESSAGE2 0x0002
#define RCMD1 0b00010001 // 即 0x11,LLC 请求 PFC 状态信息的命令头
#define RCMD2 0b00110011 // 即 0x33,LLC 请求 PFC 电压电流的命令头
1
2
3
4
5
/**********************************************Variable declaration************************************************/
sci_dg TxData[4]; // PFC发送数据载荷缓存区 (封装了high和low两个字节)
struct _SCI_Reg SciReg={0}; // SCI 通信过程控制核心结构体
unsigned short Frame1[6]={0x11}, Frame2[6]={0x33}; // 发送帧的物理数组,首字节预设为帧头
unsigned short TxFlag=SCI_TX1; // 发送帧判断标志,决定当前要回传哪种数据

代码解析

  • RCMD1 (0x11)RCMD2 (0x33) 是与 LLC 侧严格对应的命令帧头。
  • Frame1Frame2 定义了完整的 6 字节物理发送缓冲区,且初始化时就已经把第 0 个字节固化为了对应的命令头,极大地简化了后续的组帧操作。

硬件级接收中断服务函数 (ISR_SCI)

PFC 侧的数据接收完全由底层 SCI 硬件中断驱动,确保任何来自 LLC 的指令都能在第一时间被捕获。该中断函数不仅要处理正常的数据接收,还要处理通信线路上的误码和硬件错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** ===================================================================
** Funtion Name : ISR_SCI
** Description :中断接收数据函数
** Parameters : None
** Returns : Nothing
** ===================================================================
*/
__interrupt void ISR_SCI(void)
{
// 【1. 中断嵌套机制】允许更高级别的控制中断打断串口接收,保证 PFC 闭环不受影响
IER = M_INT1; // 开中断INT1
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
__asm(" NOP");
EINT;

// 【2. 硬件接收错误处理】
if (SciaRegs.SCIRXST.bit.RXERROR == 1) // 接收数据错误(如帧错误、奇偶校验错、断点错等)
{
// 软件复位 SCI 状态机,清空错误标志和乱码数据,重新恢复接收能力
SciaRegs.SCICTL1.bit.SWRESET = 0x0;
SciaRegs.SCICTL1.bit.SWRESET = 0x1;
}

// 【3. 多机通信地址位侦测 (第九位)】
// RXWAKE为1表示当前接收到的字节,其第9位(地址位)为1,说明这是一个帧头!
if (SciaRegs.SCIRXST.bit.RXWAKE == 1)
{
SciReg.RxBuffer = SciaRegs.SCIRXBUF.bit.RXDT; // 获取SCI传输首字节数据

switch (SciReg.RxBuffer & 0x0FF) // 屏蔽高位,根据首字节地址判断数据内容类型
{
case RCMD1: MesRxCmd(MESSAGE1); break; // 识别到状态请求命令
case RCMD2: MesRxCmd(MESSAGE2); break; // 识别到参数请求命令
default: break; // 其他未知数据头不予理睬,直接丢弃
}
}
else // 【4. 正常数据有效载荷接收 (第九位为0)】
{
SciReg.RxBuffer = SciaRegs.SCIRXBUF.bit.RXDT; // 暂存SCI接收到的普通数据
if (SciReg.cmd != 0) // 必须在已经接收到合法帧头的前提下,才继续存储后续信息
MesStore(SciReg.cmd);
}

PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; // 清除 SCI 所在 PIE 组的中断应答
}

接收缓存与校验状态机 (MesRxCmd & MesStore)

在中断函数中,根据接收到的是“帧头”还是“普通数据”,程序会分别调用 MesRxCmdMesStore 来推进接收状态机。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
** ===================================================================
** Interrupt handler : void MesRxCmd(int RxCmd)
** Description : 处理全新接收到的合法帧头
** ===================================================================
*/
void MesRxCmd(int RxCmd)
{
SciReg.RXCnt=0; // 接收计数器瞬间清零,准备迎接全新的一帧
setRegBits(SciReg.cmd,RxCmd); // 锁定当前正在接收的数据类型标志位
SciReg.RxFrame[SciReg.RXCnt] = SciReg.RxBuffer; // 将帧头存入接收数组的第 0 个位置
SciReg.RXCnt++; // 数据计数器累加,指向下一个待存位置
}

当帧头就绪后,后续连续到来的 5 个普通字节将通过 MesStore 函数进行累加并最终校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** ===================================================================
** Interrupt handler : void MesStore(unsigned int mesId)
** Description : 中断接收数据处理函数。连续接收6个字节后进行校验和计算。
** 校验通过后,激活对应的发送标志位准备回传。
** ===================================================================
*/
void MesStore(unsigned int mesId)
{
SciReg.RxFrame[SciReg.RXCnt] = SciReg.RxBuffer; // 将接收寄存器数据保存到帧数组中
SciReg.RXCnt++; // 接收字节数计数器累加

if (SciReg.RXCnt >= 6) // 当累计接收到第6个数据时,判定一帧数据物理接收完毕
{
SciReg.RxFrame[0] = SciReg.RxFrame[0] & 0x00FF; // 确保取有效 8 位数据

// 【核心操作:硬件级校验和比对】
// 将前 5 个字节(0~4)相加,按位取反(~),并屏蔽高位(&0x00FF)。
// 只有计算结果与接收到的第 6 个字节 (RxFrame[5]) 完全相等,才认定数据无误!
if(SciReg.RxFrame[5] == ((~(SciReg.RxFrame[0]+SciReg.RxFrame[1]+SciReg.RxFrame[2]+SciReg.RxFrame[3]+SciReg.RxFrame[4])) & 0x00FF))
{
// 校验通过,根据当前帧头类型准备回复动作
switch (mesId)
{
case MESSAGE1:
{
setRegBits(SciReg.FlagS, NEED_SENT); // 置位“需要发送”总标志
TxFlag = SCI_TX1; // 设定当前需要回传的是状态信息帧
break;
}
case MESSAGE2:
{
setRegBits(SciReg.FlagS, NEED_SENT); // 置位“需要发送”总标志
TxFlag = SCI_TX2; // 设定当前需要回传的是电压电流参数帧
break;
}
}
}
// 无论校验成功与否,一帧处理结束,强制清空接收状态机,防止死锁
SciReg.RXCnt = 0;
SciReg.cmd = 0;
}
}

发送任务调度与数据打包装载 (SciCom & TxDataRecord)

PFC 从机的发送操作并非在接收中断里死等,而是被推迟到了外部的后台轮询主函数(或慢速定时器中断)中,这种架构极大地提高了系统的实时性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
** ===================================================================
** Funtion Name : void SciCom(void)
** Description : SCI 内部通信主调度函数 (后台轮询)
** ===================================================================
*/
void SciCom(void)
{
// 判断 SCI 是否需要发送数据(即接收并校验成功了 LLC 的请求后)
if(getRegBits(SciReg.FlagS, NEED_SENT))
{
TxDataRecord(); // 第一步:将内部物理变量转换并装载至发送缓存数组中
FrameSent(); // 第二步:触发底层状态机开始按节拍发送
}
}

在装载数据时,程序将 DSP 内部的 Q 格式定点数,逆向转换为适合串口传输的高低双字节整数(分别放大 10 倍或 100 倍以保留小数精度)。这里定义了一个专用的拆位结构体:

1
2
3
4
5
typedef struct
{
unsigned char high; // 高八位
unsigned char low; // 低八位
} sci_dg;

装载函数具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
** ===================================================================
** Funtion Name : void TxDataRecord(void)
** Description : 将需要发送的物理数据转换为通信字节,存储到 TxData 缓存中
** ===================================================================
*/
void TxDataRecord(void)
{
static long SCIVacRms=0, SCIIacRms=0;

switch (TxFlag)
{
case SCI_TX1: // 准备传输 PFC 故障标志位与状态机运行状态
{
TxData[0].high = (Flag.Err >> 8) & 0x0FF; // 故障字高八位
TxData[0].low = Flag.Err & 0x0FF; // 故障字低八位
TxData[1].high = 0; // 状态字高八位(暂空)
TxData[1].low = Flag.PFCState; // 状态字低八位
break;
}
case SCI_TX2: // 准备传输交流输入电压与电流
{
// 数据换算解码:输入电压转换成实际值(扩大10倍), 输入电流转换成实际值(扩大100倍)
// " *4973>>15 " 和 " *1500>>15 " 是基于硬件采样分压比推导的还原缩放系数
SCIVacRms = Vac.Rms * 4973 >> 15;
SCIIacRms = Iac.Rms * 1500 >> 15;

// 将还原后的整数直接拆分为高八位与低八位
TxData[2].high = (unsigned char)((SCIVacRms >> 8) & 0xFF);
TxData[2].low = (unsigned char)(SCIVacRms & 0x0FF);
TxData[3].high = (unsigned char)((SCIIacRms >> 8) & 0xFF);
TxData[3].low = (unsigned char)(SCIIacRms & 0x0FF);
break;
}
}
}

组帧与硬件底层发送 (FrameSent & FrameBuild & SCI_TX)

数据准备就绪后,将由 FrameSent 状态机接管。为了防止堵塞 CPU,单帧数据的 6 个字节是按照定时器节拍“一字节一字节”地呼叫发送的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
** ===================================================================
** Funtion Name : void FrameSent(void)
** Description : 发送状态机调度
** ===================================================================
*/
void FrameSent(void)
{
switch (TxFlag)
{
case SCI_TX1:
{
// 当发送计数器为0时,说明这是一帧的全新开始,需先调用组帧函数计算校验和
if(SciReg.TXCnt==0)
FrameBuild(Frame1, &TxData[0], &TxData[1]); // 构建状态帧

SCI_TX(Frame1); // 触发底层硬件发送当前字节

// 检测到底层发送满 6 个字节触发的结束标志位
if (getRegBits(SciReg.FlagS, TX_FINISH))
{
clrRegBits(SciReg.FlagS, NEED_SENT); // 撤销需要发送总命令标志
clrRegBits(SciReg.FlagS, TX_FINISH); // 撤销发送完成标志,准备下一轮休眠
}
break;
}
case SCI_TX2:
{
if(SciReg.TXCnt==0)
FrameBuild(Frame2, &TxData[2], &TxData[3]); // 构建参数帧

SCI_TX(Frame2); // 发送

if (getRegBits(SciReg.FlagS, TX_FINISH))
{
clrRegBits(SciReg.FlagS, NEED_SENT);
clrRegBits(SciReg.FlagS, TX_FINISH);
}
break;
}
}
}

组帧与校验和计算:
在发送之前,将拆好的有效载荷按顺序填入帧数组 frame 中,并严格生成校验和字节(第 5 字节)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** ===================================================================
** Funtion Name : void FrameBuild(unsigned short *frame,sci_dg *data1,sci_dg *data2)
** Description : 构建一帧物理数据,合并载荷与计算校验和,保存于 Frame 数组中
** ===================================================================
*/
void FrameBuild(unsigned short *frame, sci_dg *data1, sci_dg *data2)
{
unsigned short cmd_total=0; int i=1;

// frame[0] 已经在初始化时固定为了 0x11 或 0x33
*(frame+1) = (unsigned char)(data1->high); // Byte 1
*(frame+2) = (unsigned char)(data1->low); // Byte 2
*(frame+3) = (unsigned char)(data2->high); // Byte 3
*(frame+4) = (unsigned char)(data2->low); // Byte 4

// 累加 Byte0 到 Byte4
for (i=0; i<5; i++)
cmd_total = cmd_total + *(frame+i);

// 按位取反,截取低 8 位作为校验和存入 Byte 5
*(frame+5) = (unsigned char)((~cmd_total) & 0x0FF);
}

硬件级单字节发送:
最后一步,将数据交由 SCI 硬件发送缓冲寄存器。关键点在于:发送帧头时,必须通过硬件寄存器强制拉高第 9 位(TXWAKE)!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
** ===================================================================
** Funtion Name : void SCI_TX(unsigned short *tx_pointer)
** Description : SCI 底层硬件发送驱动
** ===================================================================
*/
void SCI_TX(unsigned short *tx_pointer)
{
// 查询底层硬件 SCI 发送缓冲寄存器是否为空(确保上一个字节已完全移出)
if (SciaRegs.SCICTL2.bit.TXEMPTY == 1)
{
// 【关键点】如果当前发送的是第 0 个字节 (即帧头)
if(SciReg.TXCnt == 0)
SciaRegs.SCICTL1.bit.TXWAKE = 1; // 强行拉高第9位地址标志位,呼叫 LLC 准备接客

// 将对应指针的单字节数据写入硬件发送寄存器,触发底层移位发送
SciaRegs.SCITXBUF = *(tx_pointer + SciReg.TXCnt);

SciReg.TXCnt++; // 计数器步进

// 如果 6 个字节 (0~5) 已经全部轮询发送完毕
if(SciReg.TXCnt >= 6)
{
SciReg.TXCnt=0; // 游标归零
setRegBits(SciReg.FlagS, TX_FINISH); // 置位发送结束标志,通知外层状态机关闭发送业务
}
}
}

至此,PFC 侧从“中断捕捉命令 -> 硬件级校验 -> 数据转码打包 -> 时分复用分发”的完整通信闭环代码分析完毕。这套协议保证了 LLC 与 PFC 之间的高效协同,是数字电源多机架构的经典实现。

LLC的SCI通信程序深度解析

LLC的SCI通信程序深度解析

在前面的章节中,我们已经了解了 PFC 与 LLC 之间的 SCI 通信协议架构以及 PFC 从机的应答逻辑。本章我们将视角切换回 LLC(主机)侧,详细剖析 LLC 是如何作为整个通信网络的主导者,有条不紊地向 PFC 发起轮询,并精准解析 PFC 跨越隔离带传回的工作状态与电压电流数据的。

作为主机,LLC 的通信程序设计需要统筹全局。它不仅要负责发起通信节拍(每 100ms 轮询一次),还要交替发送不同的命令(请求状态或请求参数),并在接收到 PFC 的回复后,将数据解包更新至本地的系统变量中,以供整机状态机进行联锁保护判断。


主机通信时间调度与发送触发 (SciCom & TxDataRecord)

LLC 侧的通信主调度函数 SciCom 通常放置在 5ms 的定时器中断中(或主循环中以 5ms 为基准进行时间片调度)。它的核心任务是维持 100ms 的主机宏观轮询节拍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
** ===================================================================
** Funtion Name : void SciCom(void)
** SCI内部通信主函数
** Description : LLC作为主机,主动发起通信调度,掌控全局节奏
** Parameters : none
** Returns : none
** ===================================================================
*/
void SciCom(void)
{
static unsigned int Cnt=0; // 静态局部变量,用于跨周期累加时间节拍

Cnt++;
// 如果该函数按 5ms 周期调用,Cnt > 20 即代表经过了 100mS
if(Cnt>20)//100mS发送一帧数据
{
Cnt=0; // 计数器复位,准备下一个 100ms 周期
TxDataRecord();//发送空数据
setRegBits(SciReg.Flag, NEED_SENT);//置需要发送标志位,触发底层的发送状态机
}

// 实时判断 SCI 状态机是否被激活,如果需要发送数据,则进入具体的发送流程
if(getRegBits(SciReg.Flag, NEED_SENT))//判断SCI是否需要发送数据,
FrameSent();//开始发送
}

在触发发送前,程序会调用 TxDataRecord 函数来装载数据。但细心的工程师会发现,这个函数里面是空的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
** ===================================================================
** Funtion Name : void TxDataRecord(void)
** Description : 将需要发送的数据存储到TxData中
** Parameters : none
** Returns : none
** ===================================================================
*/
void TxDataRecord(void)
{
// 【深度解释】:为什么这里是空的?
// 根据本系统的协议定义,LLC 充当主机角色,它向 PFC 发送的帧本质上是“数据拉取请求指令”。
// LLC 不需要向 PFC 传递具体的电压、电流参数,因此其下发的帧有效载荷 (Byte 1 ~ Byte 4) 永远为 0。
// 固定的帧头和全 0 数据已经在全局数组初始化时完成配置,无需在此处动态装载。
}

交替发送逻辑与帧构建 (TxFlag & FrameSent & FrameBuild)

为了全面获取 PFC 的运行状态(如是否过压、过流)和实时工作参数(如交流电压、电流),LLC 必须交替发送这两种请求命令。程序中利用 TxFlag 变量实现了两种命令的“乒乓”切换机制。

1
2
unsigned short TxFlag=SCI_TX1;//发送帧判断
unsigned short Frame1[6]={0x11},Frame2[6]={0x33};//发送帧的数组,物理预置了0x11和0x33帧头

FrameSent 函数是发送过程的核心状态机。为了防止串口发送过程死等而堵塞 CPU 运行,系统将帧发送拆解,每次调用仅发送一帧里的一个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
** ===================================================================
** Funtion Name : void FrameSent(void)
** Description :SCI内部通信主函数(发送状态机)
** Parameters : none
** Returns : none
** ===================================================================
*/
void FrameSent(void)
{
switch (TxFlag) // 判断当前这 100ms 周期需要发送哪种命令
{
case SCI_TX1: // 发送组 1 (请求 PFC 状态信息)
{
// 只有在一帧的起始阶段 (TXCnt == 0) 才执行组帧与校验和计算
if(SciReg.TXCnt==0)
FrameBuild(Frame1,&TxData[0],&TxData[1]);//构建一帧数据

SCI_TX(Frame1);//调用SCI发送数据(每次仅发单字节)

// 检测到底层硬件已将一帧 (6个字节) 全部发送结束的标志
if (getRegBits(SciReg.Flag,TX_FINISH)) //若发送结束,则清发送标志位
{
clrRegBits(SciReg.Flag,NEED_SENT); //清空标志位
clrRegBits(SciReg.Flag,TX_FINISH);
TxFlag = SCI_TX2;//【核心切换】:下一次发送另外一组命令 (参数请求)
}
break;
}
case SCI_TX2: // 发送组 2 (请求 PFC 电压电流参数)
{
if(SciReg.TXCnt==0)
FrameBuild(Frame2,&TxData[2],&TxData[3]);//构建一帧数据

SCI_TX(Frame2);//调用SCI发送数据

if (getRegBits(SciReg.Flag,TX_FINISH)) //若发送结束,则清发送标志位
{
clrRegBits(SciReg.Flag,NEED_SENT); //清空标志位
clrRegBits(SciReg.Flag,TX_FINISH);
TxFlag = SCI_TX1;//【核心切换】:下一次切回状态请求,形成交替闭环
}
break;
}
}
}

组帧函数 FrameBuild 将领命执行载荷填充与校验和计算,确保数据传输的可靠性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/** ===================================================================
** Funtion Name : void FrameBuild(unsigned short *frame,sci_dg *data1,sci_dg *data2)
** 构建一帧数据,保存于Frame中
** Parameters : frame(目标数组), data1(载荷前两字节), data2(载荷后两字节)
** Returns : none
** ===================================================================
*/
void FrameBuild(unsigned short *frame,sci_dg *data1,sci_dg *data2)
{
unsigned short cmd_total=0;int i=1;

// frame[0] 已经在全局初始化时固化为 0x11 或 0x33,此处不覆盖
// 将结构体中的高低字节拆分填入物理帧的 Byte 1 到 Byte 4 中
*(frame+1)=(unsigned char)(data1->high);
*(frame+2)=(unsigned char)(data1->low);
*(frame+3)=(unsigned char)(data2->high);
*(frame+4)=(unsigned char)(data2->low);

// 计算前 5 个字节 (Byte 0 ~ Byte 4) 的累加和
for (i=0;i<5;i++)
cmd_total=cmd_total+*(frame+i);

// 按位取反 (~),并与 0x0FF 掩码进行与运算截取低 8 位,放入 Byte 5 作为校验和
*(frame+5)=(unsigned char)((~cmd_total)&0x0FF);
}

变量初始化防呆机制 (SCIValueInit)

为了防止单片机上电时,残存的脏数据引发开机误动作,系统必须进行严密的初始化。这里有一个极其重要的安全设计:故意植入非零错误码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
** ===================================================================
** Funtion Name : SCIValueInit
** Description : 相关变量初始化
** Parameters : none
** Returns : none
** ===================================================================
*/
void SCIValueInit(void)
{
unsigned char cnt=0;

clrRegBits(SciReg.Flag,NEED_SENT); //需要会送标志位清零
clrRegBits(SciReg.Flag,TX_FINISH); //发送结束标志位清零

for(cnt=0;cnt<4;cnt++) //发送寄存器载荷清零
{
TxData[cnt].high=0;
TxData[cnt].low=0;
}

PFCData.Vac=0;
PFCData.Iac=0;
// 【核心防呆设计】:将 PFC 的错误标志位默认初始化为 0x0F(非零值)。
// 这意味着 LLC 上电后,默认认为 PFC 是有故障的。
// 只有当 SCI 通信真正建立,并且 LLC 成功收到 PFC 传回的无故障指令(Err = 0)时,
// LLC 才会解除闭锁开始发波。这避免了通信断线时 LLC 盲目启动。
PFCData.Err=0x0F;
PFCData.State=0;
}

底层硬件发送驱动 (SCI_TX)

在单片机底层,串口是通过操作移位寄存器逐字节发送的。本系统为了实现多机环境下的帧头精准识别,利用了 DSP 的 地址唤醒位(TXWAKE) 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
** ===================================================================
** Funtion Name : void SCI_TX(unsigned short *tx_pointer)
** Description :SCI发送数据 (单字节硬件驱动)
** Parameters : *tx_poiter(数组,一帧数据基地址)
** Returns : none
** ===================================================================
*/
void SCI_TX(unsigned short *tx_pointer)
{
// 首先查询底层硬件 SCI 发送缓冲寄存器是否为空(即上一个字节是否已移出引脚)
if (SciaRegs.SCICTL2.bit.TXEMPTY == 1)//发送寄存器为空
{
// 【关键寻址操作】:如果是发送一帧的第 0 个字节 (即帧头)
if(SciReg.TXCnt==0)//首字节增加地址标志位
SciaRegs.SCICTL1.bit.TXWAKE = 1; // 强行拉高第九位(多机通信地址标志位)

// 将对应序号的单字节数据写入硬件发送缓冲寄存器,触发底层移位发送
SciaRegs.SCITXBUF=*(tx_pointer+SciReg.TXCnt);//开始发送

SciReg.TXCnt++; // 已发送字节计数器游标步进

// 如果一帧数据的 6 个字节 (Byte 0 ~ Byte 5) 已经全部轮询发送完毕
if(SciReg.TXCnt>=6) //发送一侦数据最后一个数据置相关标志位
{
SciReg.TXCnt=0; // 游标复位,为发送下一帧做准备
setRegBits(SciReg.Flag,TX_FINISH); // 向上层状态机置位“发送完成”标志
}
}
}

接收中断服务与多机通信寻址 (ISR_SCI & MesRxCmd)

当 LLC 发出请求后,PFC 会将自身数据打包回传。LLC 收到数据时会触发硬件接收中断。为了防止慢速的串口通信阻塞高频的 LLC 谐振腔闭环控制,该串口中断内部采用了中断嵌套机制,并在硬件级别拦截误码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** ===================================================================
** Interrupt handler : ISR_SCI
** Description :中断接收数据函数
** Parameters : None
** Returns : Nothing
** ===================================================================
*/
__interrupt void ISR_SCI(void)
{
// 【1. 中断嵌套配置】
IER = M_INT1; // 开中断INT1,允许最高优先级的控制中断打断串口接收过程
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1; // 清除组应答
__asm(" NOP"); // 流水线延时
EINT; // 开启全局中断

// 【2. 硬件错误拦截与重置】
if (SciaRegs.SCIRXST.bit.RXERROR == 1) //接收数据错误,软件复位错误数据
{
SciaRegs.SCICTL1.bit.SWRESET = 0x0; // 软件复位 SCI 状态机,清除死锁
SciaRegs.SCICTL1.bit.SWRESET = 0x1;
}

// 【3. 多机通信地址/帧头侦测】
if (SciaRegs.SCIRXST.bit.RXWAKE == 1)//接收数据为地址输入,用以判断数据内容类型,帧首字节
{
SciReg.RxBuffer=SciaRegs.SCIRXBUF.bit.RXDT;//获取SCI传输的原始数据
switch (SciReg.RxBuffer&0x0FF) //根据首字节地址判断数据存储类型
{
case RCMD1: MesRxCmd(MESSAGE1); break; // 收到状态回复帧头
case RCMD2: MesRxCmd(MESSAGE2); break; // 收到参数回复帧头
default:break; //其他未知干扰数据一律不接收
}
}
else// 【4. 有效载荷数据接收】非帧首字节 (第9位为0)
{
SciReg.RxBuffer=SciaRegs.SCIRXBUF.bit.RXDT;//暂存SCI接收数据
if (SciReg.cmd!=0) // 前提:必须已经锁定了合法的帧头指令
MesStore(SciReg.cmd); // 进入数据数组存储与校验环节
}

PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; //清中断,准备迎接下一次字节接收
}

当帧头锁定后,会立即触发初始化接收序列函数 MesRxCmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** ===================================================================
** Interrupt handler : void MesRxCmd(int RxCmd)
** Description :
** Parameters : RxCmd
** Returns : Nothing
** ===================================================================
*/
void MesRxCmd(int RxCmd)
{
SciReg.RXCnt=0; //接收计数器瞬间清零,丢弃可能存在的残余乱码
setRegBits(SciReg.cmd,RxCmd); //数据类型标志位,锁定当前正在接收的数据类型
SciReg.RxFrame[SciReg.RXCnt]=SciReg.RxBuffer; //接收数组更新,将帧首字节存入第 0 位
SciReg.RXCnt++; //数据计数器累加
}

接收数据校验与全局映射 (MesStore)

在 LLC 侧,最核心的一步就是将物理通信字节拆解、验证,并映射到系统的全局状态结构体 PFCData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/** ===================================================================
** Interrupt handler : void MesStore(unsigned int mesId)
** Description :中断接收数据处理函数,将SCI接收寄存器中的数据保存至ACK数组中,若连续接收到6个字节数据,
** 则认为一帧数据接收完成,判断接收数据类型,将对应数据赋值至对应的变量中。
** Parameters :mesId表明数据类型
** Returns : Nothing
** ===================================================================
*/
void MesStore(unsigned int mesId)
{
SciReg.RxFrame[SciReg.RXCnt]=SciReg.RxBuffer; //将SCI寄存器数据保存数组中
SciReg.RXCnt++; //接收字节数计数器累加

// 当接收到第 6 个数据时,判定一帧物理数据已接收完毕
if (SciReg.RXCnt>=6)
{
SciReg.RxFrame[0]=SciReg.RxFrame[0]&0x00FF; //只取帧头的有效低 8 位数据

// 【核心环节:硬件级累加和校验】
// 将接收到的 Byte0 到 Byte4 重新累加、取反并取低八位,对比接收到的 Byte5 (校验和字节)。
// 若完全吻合,证明数据在长距离隔离传输中毫发无损!
if(SciReg.RxFrame[5]==((~(SciReg.RxFrame[0]+SciReg.RxFrame[1]+SciReg.RxFrame[2]+SciReg.RxFrame[3]+SciReg.RxFrame[4]))&0x00FF))
{
switch (mesId)//判断接收的是ACK数据类型
{
case MESSAGE1: // 解析 PFC 回传的状态信息
{
// 高八位左移 8 位 (<< 8) 后加上低八位,拼成 16 位的 PFC 故障码
PFCData.Err=((SciReg.RxFrame[1]<<8)+SciReg.RxFrame[2]);
// 同理,拼装 PFC 的运行状态字
PFCData.State=((SciReg.RxFrame[3]<<8)+SciReg.RxFrame[4]);
break;
}
case MESSAGE2: // 解析 PFC 回传的电压电流参数
{
// 高八位与低八位拼接,还原出 PFC 测量的输入交流电压
PFCData.Vac=((SciReg.RxFrame[1]<<8)+SciReg.RxFrame[2]);
// 同理,还原出 PFC 输入交流电流
PFCData.Iac=((SciReg.RxFrame[3]<<8)+SciReg.RxFrame[4]);
break;
}
}
}

// 无论校验成功与否,必须清空接收数据计数器和状态标志,静静等待下一个新帧头
SciReg.RXCnt=0;//清接收数据Cnt,和接受标志位
SciReg.cmd=0;
}
}

至此,LLC 从发起通信请求、交替切换指令,到硬件嵌套接收、严格校验并最终解析存储数据的全过程已深度拆解完毕。

CAN通信协议解析与实现

CAN通信协议解析与实现

在现代数字电源系统中,除了电源内部各级控制器(如 PFC 与 LLC)之间需要进行 SCI 隔离通信外,电源模块作为一个整体,还需要与外部的上位机(如系统主控、BMS、测试平台等)进行可靠的参数交互与指令下发。

为了满足工业环境下的高可靠性与抗干扰要求,本系统采用了 CAN 2.0B 扩展帧协议进行外部通信。本章将详细解析这套 CAN 通信协议的帧结构定义、命令字典,并结合底层 C 语言中断和状态机代码,为您揭秘电源模块是如何与上位机实现“心跳保活”、“指令响应”及“状态上报”的。


CAN 协议基础配置与宏观设定

本系统 CAN 通信的底层硬件宏观配置遵循以下原则:

  • 帧格式:采用 CAN 2.0B 通信协议,使用 29 位扩展标识符(ID)的数据帧。
  • 字节序:所有 CAN 数据传送均采用低字节优先发送模式(小端模式)。
  • 波特率:CAN 通信物理层速率定义为 $125\text{kbit/s}$。
  • 地址分配:通信协议中明确划分了网络节点的固定地址,电源模块的固定地址(从机)为 0x80;上位机(主机)的地址为 0xA0;上位机广播命令专用地址为 0xFF
  • 接收过滤:电源模块底层的邮箱验收滤波机制被配置为:只接受与自身地址 (0x80) 相同的目标 CAN 帧,以及目标地址为 0xFF 的广播命令帧,自动屏蔽总线上的无关数据。

通信时序与保活机制:
上位机在模块运行过程中,须以一定时间间隔发送相关心跳或控制命令。为了防止上位机死机或通信线缆断开导致电源失控,电源模块内部设有时钟看门狗:如果电源模块在上电后连续 6 秒内未收到来自上位机的任何命令信号,将触发 CANLost 保护,强制进入安全等待状态;直到再次收到上位机命令后才会恢复启动输出。

应答机制:
电源模块在收到上位机发送的设置命令时,必须回复上位机一条确认信息(ACK);但当收到上位机发送的广播命令时,则无需回复任何信息,以避免总线拥堵。


协议数据单元(PDU)结构拆解

每个 CAN 数据帧包含一个用户可自行编辑的协议数据单元(PDU)。扩展帧的 PDU 由 29 位 ID 和 8 个字节的数据载荷(DATA)组成。

协议数据单元PDU

29位扩展 ID 字段定义

为了在多机网络中实现精确路由与命令分类,29 位的 ID 字段被划分为以下四个功能区:

  • 源地址 SA (0~7 bit):共 1 Byte,代表该 CAN 帧发送方的物理地址。
  • 目标地址 PS (8~15 bit):共 1 Byte,代表该 CAN 帧接收方的物理地址。
  • 命令 CMD (16~23 bit):共 1 Byte,代表该 CAN 帧的数据类型(下文有命令字典)。
  • 保留位 DP (24~28 bit):共 5 Bits,该应用中将其定义为固定值 0x18

通信路由举例:

  • 上位机发送 CAN 帧至电源模块时,ID 中的 源地址SA 为 0xA0,目标地址PS 为 0x80
  • 电源模块发送应答帧至上位机时,ID 中的 源地址SA 为 0x80,目标地址PS 为 0xA0
  • 上位机广播发送时,源地址SA 为 0xA0,目标地址PS 为广播专用地址 0xFF

CMD 命令字典定义

协议中定义了 4 种基本的 CMD 指令类型,控制着模块的行为:

CMD 代码指令描述
0x01上位机发送至模块的设置命令(如开关机、改电压电流)
0x02模块发送至上位机的回复设置命令(即 ACK)
0x20上位机发送至模块的心跳信号帧(仅作保活,无实际动作)
0x10模块发送至上位机的状态上报帧(定期上传实时参数)

8Bytes 载荷(DATA)数据域映射解析

8 Bytes 的数据内容(DATA)根据 CAN 帧中 CMD 命令字的类型不同,具有截然不同的映射关系。为了适应单片机的定点运算,所有发送的浮点参数(如电压 48.0V、电流 5.0A)均放大了 100 倍转化为整数(如 4800 和 500)传输。

1. 上位机发送设置命令至模块 (CMD=0x01):

  • Byte 0: 预留
  • Byte 1: 开关机指令(0 代表关机,1 代表开机)
  • Byte 2, Byte 3: 参考电压设定值(Byte3 为高位)。例如设为 48V,则该十六进制数据对应的十进制为 4800。
  • Byte 4, Byte 5: 参考电流设定值(Byte5 为高位)。例如设为 5A,则该数据为 500。
  • Byte 6, Byte 7: 预留

2. 模块回复上位机设置命令 (CMD=0x02):

  • Byte 0: 成功与否标志位(0 代表成功接收并执行;1 代表数值越界设置失败)
  • Byte 1 ~ Byte 5: 均与上位机下发的命令原样一致,作为数据回显确认。
  • Byte 6, Byte 7: 预留

3. 上位机发送心跳帧 (CMD=0x20):

  • Byte 0 ~ Byte 7: 全部填充为 0,仅仅是为了让接收中断刷新 LostCnt 防掉线计时器。

4. 模块发送状态上报帧 (CMD=0x10):

  • Byte 0: PFC 状态信息(运行状态位合集)
  • Byte 1: PFC 故障信息(错误码)
  • Byte 2: LLC 状态信息
  • Byte 3: LLC 故障信息
  • Byte 4, Byte 5: 输出电压实际采样值(Byte5 为高位,实际值*100)。
  • Byte 6, Byte 7: 输出电流实际采样值(Byte7 为高位,实际值*100)。

底层硬件中断接收流程

为了节约宝贵的 CPU 算力和中断响应时间,模块 CAN 通信对数据接收使用中断方式响应。为了不影响高频 LLC 控制环路,该中断的优先级被设置得较低。
在接收中断服务函数 __interrupt void sMailboxISR(void) 中,程序绝不做任何耗时的业务数据处理或参数校验,它的唯一任务就是:“将接收到的邮箱数据原封不动地搬运转存至内存中的 PDU 结构体中”,并置位“接收完成标志位”,唤醒主程序。

中断接收程序流程图

流程如下:当硬件邮箱发生接收中断时,CPU 读取产生中断的信箱号。接着,判断该邮箱收到的目标地址是否为广播地址。如果是广播邮箱,则置位“接收广播标志”;否则置位常规“接收标志”。随后,火速将数据拷贝至 PDU,结束中断。


CAN 主程序任务调度流

由于繁重的数据解析不能放在中断里,电源系统专门设计了后台 CAN 主调度程序来统筹一切。

CAN主程序程序流程图

如上图所示,CAN 主程序的运行逻辑如下:

  1. 首先检测全局接收标志位是否为 1(说明刚发生过中断,收到了新数据)。
  2. 如果有新数据,立刻将接收标志位清零,同时最关键的是:将 CAN 丢失保护计时器 (CanDat.LostCnt) 清零,撤销通信掉线保护报警
  3. 判断接收到的命令 CMD 类型。如果仅仅是心跳帧,则只刷新掉线时间不做动作。如果是设置命令 (CMD=0x01),则调用下文的转设置命令子程序
  4. 在处理完设置指令后,系统会检查此次接收的是否为广播命令。如果不是广播命令,按照协议规定,模块必须调用转回复设置命令子程序 (CMD=0x02) 向上位机发送应答。如果是广播,则静默结束。
  5. 在没有收到上位机命令的时候,程序也没有闲着。调度器内部维护了一个 1 秒钟的定时器,当该计时器溢出时,模块会自动触发转状态上报程序 (CMD=0x10),将自身的电压电流和故障码打包并向总线推送。

程序业务逻辑实现

转设置命令子程序 (CANSetCmd)

当主程序确定这是一条合法的设置指令后,进入该子流程。首先将 PDU 中的裸数据拷贝到用户态 CanDat 结构体中。

设置命令程序

随后程序依次执行参数解码:

  • 提取 Byte1,执行底层状态机的开/关机切换操作。
  • 提取 Byte2, Byte3,将其拼装为 16 位整数,并除以 100 换算为物理参考电压,赋值给 LLC 闭环控制器的电压环基准变量。
  • 提取 Byte4, Byte5 进行参考电流限幅阈值转换。
    值得一提的是,在执行赋值前,程序必须执行越界安全校验。如果上位机下发的电压值超出了电源模块的硬件最大承受范围,程序会拒绝执行该指令,并将自身的设置应答标志位标记为“失败 (1)”。

回复设置命令子程序 (CANAck)

在参数设置完毕后,模块需组装应答帧 (CMD=0x02)。

回复设置命令程序流程图

组装流程极度格式化:

  1. 配置 PDU 标识符:设定 CMD=0x02,强制目标地址 PS=0xA0(上位机),源地址 SA=0x80(模块自身)。
  2. 装载 CAN ID 硬件寄存器。
  3. 将包含“成功/失败标志位”和原始指令回显数据填入 8 字节的数据发送寄存器中。
  4. 置位发送请求触发位,调用 CAN 底层外设发送完毕。

LED 状态机联动指示

由于 CAN 通信接管了模块的所有宏观控制,面板上的 LED 状态指示灯也会随着 CAN 下发的指令及保护状态实时变化。这段极其直观的 C 代码展示了电源外设如何与内部状态机 (DF.SMFlag) 紧密联动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/** ===================================================================
** Funtion Name :void LEDShow(void)
** Description : LED显示函数-初始化与等待启动状态,红黄绿全亮
** 启动状态,黄绿亮--运行状态,绿灯亮--故障状态,红灯亮
** ===================================================================*/
//输出状态灯硬件引脚宏定义
#define SET_LED_G() GpioDataRegs.GPBSET.bit.GPIO32 = 1 //绿灯亮
#define SET_LED_Y() GpioDataRegs.GPASET.bit.GPIO22 = 1 //黄灯亮
#define SET_LED_R() GpioDataRegs.GPBSET.bit.GPIO33 = 1 //红灯亮
#define CLR_LED_G() GpioDataRegs.GPBCLEAR.bit.GPIO32= 1//绿灯灭
#define CLR_LED_Y() GpioDataRegs.GPACLEAR.bit.GPIO22= 1//黄灯灭
#define CLR_LED_R() GpioDataRegs.GPBCLEAR.bit.GPIO33 = 1//红灯灭

void LEDShow(void)
{
CanDat.RecordCnt++; // CAN 1秒状态上报数据计时器步进
CanDat.LostCnt++; // CAN 6秒通信掉线(丢失)保护计时器步进

switch(DF.SMFlag)
{
case Init : // 初始化状态,全量点亮测试灯管是否完好
{
SET_LED_G();
SET_LED_Y();
SET_LED_R();
break;
}
case Wait :StateMWait(); // 待机等待状态,红黄绿全亮
{
SET_LED_G();
SET_LED_Y();
SET_LED_R();
break;
}
case Rise :StateMRise(); // LLC 正在执行软启动扫频升压,红灯熄灭,黄绿亮起
{
SET_LED_G();
SET_LED_Y();
CLR_LED_R();
break;
}
case Run :StateMRun(); // 平稳运行状态,仅保留绿灯常亮
{
SET_LED_G();
CLR_LED_Y();
CLR_LED_R();
break;
}
case Err :StateMErr(); // 遭遇任何内部保护或 CAN 通信丢失报警,直接亮红灯示警
{
CLR_LED_G();
CLR_LED_Y();
SET_LED_R();
break;
}
}
}
CAN通信初始化与底层代码实现

CAN通信初始化与底层代码实现

在上一章中,我们详细解析了电源模块与上位机之间 CAN 通信的 29 位扩展帧协议架构以及宏观的控制逻辑。协议的完美运行,离不开 DSP 底层硬件外设的精准支撑。

本章将深入到 DSP 的底层寄存器级别,为您逐行剖析 CAN 通信模块的初始化流程。我们将从核心数据结构的定义出发,一步步揭秘如何配置 GPIO 复用、如何推导并设定 125kbit/s 的波特率,以及如何通过配置独立的发送与接收邮箱(Mailbox)来实现强大的硬件级报文过滤与中断管理。


核心数据结构与宏定义

为了在 C 语言中高效地打包、解包和管理上一章定义的 29 位扩展帧以及 8 字节有效载荷,系统定义了两个核心的全局结构体:_PDU(协议数据单元)和 _can_para(模块本机状态参数),以及对应的命令类型宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern struct _can_para CanDat;

// CAN通信协议数据单元(PDU)结构体定义
struct _PDU
{
unsigned char P; // P为优先权
unsigned char R; // R位保留位:备今后开发使用,本标准设为0。
unsigned char DP; // 为数据页
unsigned char PF; // PF为PDU格式:用来确定PDU的格式,以及数据域对应的参数组编号。
unsigned char PS; // PS为PDU特定格式:PS值取决于PDU格式。本标准中采用PDU1格式,PS值为目标地址
unsigned char SA; // SA为源地址:发送此报文的源地址
unsigned char data[8]; // DATA为数据域:若给定参数组数据长度≤8字节,按照8字节进行传输,缺省值为00H。若给定参数组数据长度为9 ~1785,数据传输需要多个CAN数据帧,通过协议传输功能通信,详见7.5的规定。
};

// 模块本身相关信息结构体
struct _can_para
{
unsigned long LostCnt; // Heatbeat计时,若在一定时间内没被清零,溢出则发生故障
unsigned long RecordCnt; // 遥信遥测数据恢复时间
unsigned char OutEn; // 开关机使能
unsigned int Voref; // 参考电压
unsigned int Ioref; // 参考电流
};

与之对应的,程序将常用的 CMD 命令字定义为了便于识别的宏:

1
2
3
4
#define PF_SET_CMD 0x01         // 命令帧
#define PF_SET_CMD_ACK 0x02 // 命令帧回复
#define PF_HEART_BEAT 0x20 // 心跳帧
#define PF_RECORD_VALUE 0x10 // 状态上报帧

CAN 初始化配置流程拆解

初始化 CAN 模块是一项涉及系统时钟、引脚复用和多重寄存器解锁的严谨工程。根据系统设计规范,CAN 初始化的标准流程严格包含以下步骤:

  • 配置GPIO30上拉方式;
  • 配置GPIO31上拉方式;
  • 配置GPIO30上拉异步方式;
  • 配置GPIO30为CAN接收功能;
  • 配置GPIO31为CAN发送功能;
  • 使能CAN发送功能;
  • 使能CAN接收功能;
  • 配置SCB位,增强型CAN2.0B模式;
  • 清发送标志位;
  • 清接收标志位;
  • 清中断0标志位;
  • 清中断1标志位;
  • 关闭所有邮箱;
  • 初始化复位32个邮箱,ID位和控制位,数据位为0,数据长度为8,无远程帧;
  • 关闭邮箱中断功能;
  • 所有邮箱都选择Int0中断;
  • 异常中断在频道Int1;
  • 配置CCR=1和ABO=1;
  • 等待CCE标志位置1;
  • 根据手册配置CAN通信速率为125kbit;
  • 配置CCR=0,STM=0,DBO=0;
  • 等待CCE标志位清0;
  • 关闭所有邮箱功能;
  • 配置邮箱0为发送邮箱;
  • 配置邮箱1为接收邮箱;
  • 配置邮箱2为接收邮箱;
  • 配置邮箱0的初始ID;
  • 配置邮箱1的初始ID;
  • 配置邮箱2的初始ID;
  • 使能邮箱1的屏蔽功能;
  • 使能邮箱2的屏蔽功能;
  • 配置邮箱1的屏蔽功能屏蔽位;
  • 配置邮箱2的屏蔽功能屏蔽位;
  • 配置邮箱0为拓展帧;
  • 配置邮箱1为拓展帧;
  • 配置邮箱2为拓展帧;
  • 使能0为邮箱;
  • 使能1为邮箱;
  • 使能2为邮箱;
  • 打开邮箱1和邮箱2的接收中断;

CAN 初始化代码深度解析 (CanInit)

上述的数十项严苛配置步骤,最终汇聚成了下面这段极其健壮的底层 C 语言初始化代码。其中波特率的推导与邮箱的寻址滤波配置是重中之重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/*
** ===================================================================
** Funtion Name : void CanInit(void)
** Description :Can初始化程序
** Parameters : none
** Returns : none
** ===================================================================
*/
#define CAN_ADD 0x80//模块地址
#define BROADCAST 0xFF//广播地址
#define MONITOR_ADD 0xA0//上位机地址
void CanInit(void)
{
//部分Can的寄存器操作需要影子寄存器,不能直接修改寄存器
struct ECAN_REGS ECanaShadow;
//邮箱寄存器
volatile struct MBOX *Mbox_pointer;
int MboxCnt=0;

EALLOW;
//配置对应的CAN 的IO口
GpioCtrlRegs.GPAPUD.bit.GPIO30 = 0; // Enable pull-up for GPIO30 (CANRXA)
GpioCtrlRegs.GPAPUD.bit.GPIO31 = 0; // Enable pull-up for GPIO31 (CANTXA)
GpioCtrlRegs.GPAQSEL2.bit.GPIO30 = 3; // Asynch qual for GPIO30 (CANRXA)
GpioCtrlRegs.GPAMUX2.bit.GPIO30 = 1; // Configure GPIO30 for CANRXA operation
GpioCtrlRegs.GPAMUX2.bit.GPIO31 = 1; // Configure GPIO31 for CANTXA operation

//基础配置
//配置CAN发送功能, Configure eCAN TX pins for CAN operation using eCAN regs
ECanaShadow.CANTIOC.all = ECanaRegs.CANTIOC.all;
ECanaShadow.CANTIOC.bit.TXFUNC = 1;
ECanaRegs.CANTIOC.all = ECanaShadow.CANTIOC.all;
//配置CAN接收功能,Configure eCAN RX pins for CAN operation using eCAN regs
ECanaShadow.CANRIOC.all = ECanaRegs.CANRIOC.all;
ECanaShadow.CANRIOC.bit.RXFUNC = 1;
ECanaRegs.CANRIOC.all = ECanaShadow.CANRIOC.all;

// Configure eCAN for ecan(CAN2.0B) mode enable can
ECanaShadow.CANMC.all = ECanaRegs.CANMC.all;
ECanaShadow.CANMC.bit.SCB = 1;
ECanaRegs.CANMC.all = ECanaShadow.CANMC.all;

//请发送ACK标志位,请接收挂起标志位,清中断标志位
// TAn, RMPn, GIFn bits are all zero upon reset and are cleared again
// as a matter of precaution.
ECanaRegs.CANTA.all = 0xFFFFFFFF; // Clear all TAn bits
ECanaRegs.CANRMP.all = 0xFFFFFFFF; // Clear all RMPn bits
ECanaRegs.CANGIF0.all = 0xFFFFFFFF; // Clear all interrupt flag bits
ECanaRegs.CANGIF1.all = 0xFFFFFFFF;

//初始化所有邮箱内容,上电复位后邮箱状态未定,需要初始化
// 改写邮箱地址要求先关闭对应邮箱,先将所有邮箱关闭
//Disable all Mailboxes, Required before writing the MSGIDs
ECanaShadow.CANME.all = 0;
ECanaRegs.CANME.all = ECanaShadow.CANME.all;
Mbox_pointer = &ECanaMboxes.MBOX0;
for(MboxCnt= 0;MboxCnt<32;MboxCnt++)
{
//完备性操作,将所有可能的随机数刷新到确定值0
Mbox_pointer->MSGID.all = 0;
Mbox_pointer->MSGCTRL.all = 0;
Mbox_pointer->MDH.all = 0;
Mbox_pointer->MDL.all = 0;
//为所有邮箱配置
Mbox_pointer->MSGCTRL.bit.DLC = 8;//数据传输长度8字节。
Mbox_pointer->MSGCTRL.bit.RTR = 0;//没有远程帧
Mbox_pointer = Mbox_pointer + 1;
}

ECanaRegs.CANMIM.all = 0; //关闭所有邮箱的中断使能位,具体的中断使能情况,将通过CanMaiBoxPreset将邮箱的配置初始值整体写入。
ECanaRegs.CANMIL.all = 0; //中断频道,mail isr => int0,所有邮箱都选择Int0中断。集中到一个中断程序内处理。
ECanaRegs.CANGIM.all = 0x05; //just enabel int0,异常中断在频道Int1。所以所有异常中断没有被使能。

// Configure bit timing parameters for eCANA Set CCR = 1
ECanaShadow.CANMC.all = ECanaRegs.CANMC.all;
ECanaShadow.CANMC.bit.CCR = 1 ;
ECanaShadow.CANMC.bit.ABO = 1; // Bus自恢复。
ECanaRegs.CANMC.all = ECanaShadow.CANMC.all;
// Wait until the CPU has been granted permission to change
// the configuration registers
do
{
ECanaShadow.CANES.all = ECanaRegs.CANES.all;
} while(ECanaShadow.CANES.bit.CCE != 1 ); // Wait for CCE bit to be set

//配置CAN通信波特率
//复位时基寄存器
//(60M/2)/((23+1)*(1+1+6+1+1)=125K
ECanaShadow.CANBTC.all = 0;
ECanaShadow.CANBTC.bit.BRPREG = 23;
ECanaShadow.CANBTC.bit.TSEG2REG = 1;
ECanaShadow.CANBTC.bit.TSEG1REG = 6;
ECanaShadow.CANBTC.bit.SAM = 0;//The CAN module samples three times and make a majority decision.
ECanaRegs.CANBTC.all = ECanaShadow.CANBTC.all;

// Configure bit timing parameters for eCANA Set CCR = 0
ECanaShadow.CANMC.all = ECanaRegs.CANMC.all;
ECanaShadow.CANMC.bit.CCR = 0 ;
ECanaShadow.CANMC.bit.STM = 0; // diable CAN for self-test mode
ECanaShadow.CANMC.bit.DBO = 0; //低字节优先
ECanaRegs.CANMC.all = ECanaShadow.CANMC.all;
// Wait until the CPU no longer has permission to change
// the configuration registers
do
{
ECanaShadow.CANES.all = ECanaRegs.CANES.all;
} while(ECanaShadow.CANES.bit.CCE != 0 ); // Wait for CCE bit to clear

//Disable all Mailboxes, Required before writing the MSGIDs
ECanaShadow.CANME.all = 0;
ECanaRegs.CANME.all = ECanaShadow.CANME.all;

// Configure Mailboxes 0 as Tx, 1as Rx
ECanaRegs.CANMD.bit.MD0 = 0;//发送
ECanaRegs.CANMD.bit.MD1= 1;//点对点接收
ECanaRegs.CANMD.bit.MD2= 1;//广播接收

//配置帧头
ECanaMboxes.MBOX0.MSGID.all = ( ECanaMboxes.MBOX0.MSGID.all | CAN_ADD );
ECanaMboxes.MBOX1.MSGID.all = ( ECanaMboxes.MBOX1.MSGID.all | (((unsigned int)CAN_ADD)<<8)|MONITOR_ADD);
ECanaMboxes.MBOX2.MSGID.all = ( ECanaMboxes.MBOX2.MSGID.all | (((unsigned int)BROADCAST)<<8)|MONITOR_ADD);
//启动MASK功能,启动MASK功能,只接收自身地址的命令和广播命令(分组命令控制)
ECanaMboxes.MBOX1.MSGID.bit.AME = 1;
ECanaMboxes.MBOX2.MSGID.bit.AME = 1;
//只判断目标地址屏蔽-只接受上位机和自身地址都相同的数据
ECanaLAMRegs.LAM1.all = 0x1FFF0000;
ECanaLAMRegs.LAM2.all = 0x1FFF0000;
//拓展帧
ECanaMboxes.MBOX0.MSGID.bit.IDE=1;//拓展帧
ECanaMboxes.MBOX1.MSGID.bit.IDE=1;//拓展帧
ECanaMboxes.MBOX2.MSGID.bit.IDE=1;//拓展帧
//Enable MBOX0 and MBOX1/MBOX2
ECanaRegs.CANME.bit.ME0 = 1;
ECanaRegs.CANME.bit.ME1= 1;
ECanaRegs.CANME.bit.ME2= 1;

//M1/2进入接收中断,接收中断,发送不做中断处理
ECanaRegs.CANMIM.all = 0x06;
ECanaRegs.CANMIL.all = 0;
ECanaRegs.CANGIM.all = 0x05;
EDIS;
}
CAN通信程序分析

CAN通信程序分析

在完成了 CAN 通信底层寄存器的初始化配置之后,系统已经具备了与上位机进行物理层和数据链路层交互的能力。本章将详细剖析 CAN 通信应用层的核心业务逻辑,包括:中断接收、指令分发调度、参数安全校验、应答机制,以及实时遥信遥测数据的组包与上报。


CAN 中断接收与数据转存 (sMailboxISR)

为了最大程度保证 LLC 高频控制环路的实时性,CAN 接收中断(sMailboxISR)被设计得极为精简。该中断函数内绝对不进行任何业务逻辑的判断与数据解析,它的唯一任务就是:在硬件产生接收中断的瞬间,将底层邮箱(Mailbox)中的数据快速“搬运”到全局协议数据单元结构体 PDU 中,并置位标志位交由主循环处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
** ===================================================================
** Funtion Name :__interrupt void sMailboxISR(void)
** Description :CAN中断函数
** Can中断,中断只完成中完成数据的转移。数据的解析,由绑定的数据结构自动完成。
** Parameters : none
** Returns : none
** ===================================================================
*/
unsigned char CANRxFlag= 0;//接收数据中断标志位
unsigned char CANbroadFlag=0;//广播标志位

__interrupt void sMailboxISR(void)
{
// 【1. 中断嵌套机制】开启 INT1 组中断,确保更底层的 20us 发波保护中断可以打断本程序
IER = M_INT1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
__asm(" NOP");
EINT;

// CAN中断影子寄存器,用于一次性读取中断状态
union CANGIF0_REG CANGIF0_Shandow;
unsigned int ISR_MailBoxNum; // 触发本次中断的邮箱号

// 影子寄存器获取中断寄存器数据
CANGIF0_Shandow.all = ECanaRegs.CANGIF0.all;
// 获取具体是哪个邮箱触发了中断 (MIV0 字段包含最高优先级的挂起中断邮箱号)
ISR_MailBoxNum = CANGIF0_Shandow.bit.MIV0;

// 全局置位接收数据标志位,通知主程序有新数据到达
CANRxFlag =1;

// 根据上一章初始化配置,邮箱 2 专门用于接收广播地址 (0xFF) 的数据
if(ISR_MailBoxNum == 2)
CANbroadFlag = 1; // 广播标志位置位,后续协议规定广播命令不需要回复 ACK

// 【2. 数据解包与转存】
// 将 29 位的 MSGID 寄存器通过移位和按位与(&)操作,精准剥离出协议定义的各个字段
PDU.P = (unsigned char)( ( ECanaMboxes.MBOX1.MSGID.all >>26 ) & 0x00000007 ); // 优先级
PDU.R = (unsigned char)( ( ECanaMboxes.MBOX1.MSGID.all >>25 ) & 0x00000001 ); // 保留位
PDU.DP = (unsigned char)( ( ECanaMboxes.MBOX1.MSGID.all >>24 ) & 0x00000001 ); // 数据页
PDU.PF = (unsigned char)( ( ECanaMboxes.MBOX1.MSGID.all >>16 ) & 0x000000FF ); // CMD 命令字
PDU.PS = (unsigned char)( ( ECanaMboxes.MBOX1.MSGID.all >>8 ) & 0x000000FF ); // 目标地址
PDU.SA = (unsigned char)( ECanaMboxes.MBOX1.MSGID.all & 0x000000FF ); // 源地址

// 转存高低各 32 位的数据有效载荷 (共 8 字节)
PDU.data[0]=(unsigned char)( ( ECanaMboxes.MBOX1.MDL.all>>24 ) & 0x0000000FF);
PDU.data[1]=(unsigned char)( ( ECanaMboxes.MBOX1.MDL.all>>16 ) & 0x0000000FF);
PDU.data[2]=(unsigned char)( ( ECanaMboxes.MBOX1.MDL.all>>8 ) & 0x0000000FF);
PDU.data[3]=(unsigned char)( ECanaMboxes.MBOX1.MDL.all & 0x0000000FF );
PDU.data[4]=(unsigned char)( ( ECanaMboxes.MBOX1.MDH.all>>24 ) & 0x0000000FF);
PDU.data[5]=(unsigned char)( ( ECanaMboxes.MBOX1.MDH.all>>16 ) & 0x0000000FF);
PDU.data[6]=(unsigned char)( ( ECanaMboxes.MBOX1.MDH.all>>8 ) & 0x0000000FF);
PDU.data[7]=(unsigned char)( ECanaMboxes.MBOX1.MDH.all & 0x0000000FF );

// 【3. 清除中断标志,恢复硬件接收能力】
// 向 CANRMP (Receive Message Pending) 的对应位写 1,即可清除挂起标志
ECanaRegs.CANRMP.all = 0x1Lu<<ISR_MailBoxNum;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; // 响应系统级 PIE 中断
}

CAN 数据处理与调度核心 (CANMessage)

主循环通过轮询 CANRxFlag 标志位来判断是否需要处理新数据。一旦检测到新数据,CANMessage 函数将作为中央调度器,执行掉线看门狗“喂狗”、指令散转以及应答触发。
同时,它也兼顾了在没有外部请求时,主动定时上报模块状态的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/*
** ===================================================================
** Funtion Name : void CANMessage(void)
** Description : CAN数据处理与分发调度
** Parameters : none
** Returns : none
** ===================================================================
*/
#define ERR_NULL 0 //无错误 (执行成功)
#define FAIL 1 //执行失败 (如参数越界)

void CANMessage(void)
{
unsigned char errType = ERR_NULL;

// 当检测到有新数据接收时
if( CANRxFlag ==1)
{
// 1. 接收标志位清0,避免重复处理
CANRxFlag = 0;

// 2. 核心保活机制:喂狗。CAN丢失计时清零
CanDat.LostCnt=0;
// 有数据接收,立即清除系统可能残留的 CAN 丢失故障保护状态
clrRegBits(DF.ErrFlag,F_CAN_LOST);

// 3. 判断接收数据的类型 (即 PDU 格式字段 PF,也即 CMD)
switch(PDU.PF)
{
// 上位机下发的是参数设置命令
case PF_SET_CMD :
errType = CANSetCmd(); // 调用解析与校验函数,并获取执行结果
break;
// 上位机下发的仅仅是心跳帧
case PF_HEART_BEAT :
// 不做任何数据解析处理,因为上面已经清零了 LostCnt 实现了保活
break;
}

// 4. 应答 (ACK) 机制拦截
if(CANbroadFlag==1) // 如果刚刚收到的是广播命令
CANbroadFlag=0; // 清除广播标志,严禁回复,防止总线上的所有从机同时回复导致死锁
else
CANAck(errType); // 点对点通信,必须将执行结果 (成功/失败) 回复给上位机
}
else
{
// 5. 主动上报机制:遥信遥测数据
// 如果当前没有正在处理的接收数据,则检查主动上报计时器
// 200 次计时,依托外部调用频率(如 5ms 周期),200 * 5ms = 1秒上报一次数据
if(CanDat.RecordCnt> 200)
{
CanDat.RecordCnt=0;
CanRecord(); // 触发遥信遥测数据组包与发送
}
}
}

系统状态指示与超时看门狗 (LEDShow)

虽然该函数名为 LEDShow,但它不仅负责驱动前面板的红、黄、绿状态指示灯,更是整个 CAN 通信时间基准的“脉搏”。通信超时计数器(LostCnt)和上报周期计数器(RecordCnt)都是在这个按严格周期(5ms)调用的函数中进行累加的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/** ===================================================================
** Funtion Name :void LEDShow(void)
** Description : LED显示函数及通信计时基准
** 初始化与等待启动状态,红黄绿全亮
** 启动状态,黄绿亮 -- 运行状态,绿灯亮 -- 故障状态,红灯亮
** Parameters :
** Returns :
** ===================================================================*/
// 输出状态灯引脚操作宏定义
#define SET_LED_G() GpioDataRegs.GPBSET.bit.GPIO32 = 1//绿灯亮
#define SET_LED_Y() GpioDataRegs.GPASET.bit.GPIO22 = 1//黄灯亮
#define SET_LED_R() GpioDataRegs.GPBSET.bit.GPIO33 = 1//红灯亮
#define CLR_LED_G() GpioDataRegs.GPBCLEAR.bit.GPIO32= 1//绿灯灭
#define CLR_LED_Y() GpioDataRegs.GPACLEAR.bit.GPIO22= 1//黄灯灭
#define CLR_LED_R() GpioDataRegs.GPBCLEAR.bit.GPIO33 = 1//红灯灭

void LEDShow(void)
{
// 【通信核心基准计时】
CanDat.RecordCnt++; // CAN 遥测上报数据计时器累加
CanDat.LostCnt++; // CAN 通信丢失报警(看门狗)计数器累加

// 【面板状态灯联动逻辑】
switch(DF.SMFlag)
{
case Init : // 初始化状态,全亮测试硬件
{
SET_LED_G();
SET_LED_Y();
SET_LED_R();
break;
}
case Wait :StateMWait(); // 待机保护状态
{
SET_LED_G();
SET_LED_Y();
SET_LED_R();
break;
}
case Rise :StateMRise(); // LLC 扫频软启动状态 (红灯灭,黄绿亮)
{
SET_LED_G();
SET_LED_Y();
CLR_LED_R();
break;
}
case Run :StateMRun(); // 平稳闭环运行状态 (仅绿灯常亮)
{
SET_LED_G();
CLR_LED_Y();
CLR_LED_R();
break;
}
case Err :StateMErr(); // 系统故障保护锁死状态 (仅红灯常亮报警)
{
CLR_LED_G();
CLR_LED_Y();
SET_LED_R();
break;
}
}
}

设置命令解析与安全限幅检查 (CANSetCmd & SetCmdUpdate)

接收到上位机的控制指令后,必须先将 8 字节裸数据重组,然后进行严格的安全边界校验(防止上位机误发极值导致炸机),最后再将物理量转换为 DSP 内部的 Q 格式定点控制量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
** ===================================================================
** Funtion Name : unsigned char CANSetCmd(void)
** Description : 解析重组上位机下发的设置命令
** ===================================================================
*/
unsigned char CANSetCmd(void)
{
unsigned char errType = ERR_NULL;

// CAN数据转存结构体 CanDat
CanDat.OutEn = PDU.data[1]; // Byte 1: 开关机指令
// 将 Byte2 与 Byte3 拼接为 16 位电压参考设定值 (高位左移8位后按位或)
CanDat.Voref = ((((unsigned int)PDU.data[3])<<8) | PDU.data[2]);
// 同理拼接参考电流
CanDat.Ioref = ((((unsigned int)PDU.data[5])<<8) | PDU.data[4]);

// 调用更新与校验函数
errType = SetCmdUpdate();
return errType; // 将校验结果返回,供后续 ACK 应答使用
}

/*
** ===================================================================
** Funtion Name : unsigned char SetCmdUpdate(void)
** Description : 对命令数据进行限幅校验,转换成底层环路控制参数
** ===================================================================
*/
unsigned char SetCmdUpdate(void)
{
unsigned char errType = ERR_NULL;

// 1. 开关机指令校验
// 判断 Byte 1 只有最低位有效,其余位必须为 0 (掩码 0xFE = 11111110)
if((CanDat.OutEn & 0xFE) == 0)
{
if(CanDat.OutEn)
setRegBits(DF.CtrFlag, F_OUT_EN); // 允许发波
else
clrRegBits(DF.CtrFlag, F_OUT_EN); // 强制停机
}
else // 数据格式非法
{
errType = FAIL; // 返回错误码,拒绝执行
return errType;
}

// 2. 参考电压物理边界校验与定点化
// 上位机下发数据放大了 100 倍。这里限制目标电压必须在 24.00V 到 48.00V 之间
if((CanDat.Voref > 4800) || (CanDat.Voref < 2400))
{
errType = FAIL;
return errType; // 数据越界,拒绝执行
}
else
{
// 定点化缩放:将 0~4800 的上位机数据转换为 DSP 内部 Q 格式。
// 公式:$V_{DSP} = \frac{V_{ref} \times 4706}{2^{10}}$
ID2DCtrValue.CanVoref = ((unsigned long)CanDat.Voref)*4706>>10;
}

// 3. 参考电流物理边界校验与定点化
// 限制目标电流必须在 0.10A 到 10.00A 之间
if((CanDat.Ioref > 1000) || (CanDat.Ioref < 10))
{
errType = FAIL;
return errType;
}
else
{
// 定点化缩放转换
// 公式:$I_{DSP} = \frac{I_{ref} \times 3813}{2^{8}}$
ID2DCtrValue.CanIoref = ((unsigned long)CanDat.Ioref)*3813>>8;
}

return errType;
}

构造命令应答与发送引擎 (CANAck & CANTx)

只要模块收到非广播指令,就必须将执行结果(ERR_NULLFAIL)封装并回送上位机。应答的核心在于目标地址与源地址的对调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
** ===================================================================
** Funtion Name : void CANAck(unsigned char type)
** Description : 回复上位机设置命令的结果
** Parameters : type (成功与否的状态字)
** ===================================================================
*/
void CANAck(unsigned char type)
{
unsigned long MailBoxID = 0;
unsigned long temp=0;

// 1. 组装 29 位 ID:更改 CMD 命令字为 ACK 类型 (0x02)
temp = PF_SET_CMD_ACK;
temp = temp<<16;

// 提取原 MSGID 的最高 5 位 (优先级等保留位,掩码 0xF8000000)
MailBoxID = ECanaMboxes.MBOX0.MSGID.all & 0xF8000000;
MailBoxID = MailBoxID | temp;

// 转换 ID 路由:将接收时的源地址 (上位机) 作为本次发送的目标地址 (左移8位)
temp = MONITOR_ADD;
MailBoxID = MailBoxID | (temp<<8);
// 将接收时的目标地址 (模块自身) 作为本次发送的源地址 (低 8 位)
MailBoxID = MailBoxID | CAN_ADD;

temp = type; // 缓存执行结果 (0或1)

EALLOW;
// 配置邮箱前必须先关闭发送使能
ECanaRegs.CANME.bit.ME0= 0;
ECanaMboxes.MBOX0.MSGID.all = MailBoxID;

// 2. 装载应答数据载荷
// 将上位机下发时的原数据 (MDL) 原样回显,并将成功/失败标志位强行插入到 Byte 0 中 (左移 31 位)
ECanaMboxes.MBOX0.MDL.all = ECanaMboxes.MBOX1.MDL.all | (temp<<31) ;
// 后四个字节 (MDH) 原样回显
ECanaMboxes.MBOX0.MDH.all = ECanaMboxes.MBOX1.MDH.all ;

// 重新使能发送邮箱 0
ECanaRegs.CANME.bit.ME0= 1;
EDIS;

CANTx(); // 触发底层硬件发送
}

/*
** ===================================================================
** Funtion Name : void CANTx(void)
** Description : 底层硬件 CAN 发送触发引擎
** ===================================================================
*/
void CANTx(void)
{
unsigned long TxStatus;

// 读取邮箱 0 的发送请求状态寄存器
TxStatus = ECanaRegs.CANTRS.bit.TRS0;

// 表明该邮箱没有在发送进程中,空闲可用,直接请求发送 (置 1)
if(TxStatus == 0)
ECanaRegs.CANTRS.bit.TRS0=1;
else
{
// 异常处理:表明邮箱卡死在上一帧没有完成发送。
// 应强行中止前一次发送,重启该邮箱发送通道。
ECanaRegs.CANTRR.bit.TRR0=1; // Transmit Reset
ECanaRegs.CANTA.bit.TA0=1; // Clear Transmission Acknowledge
}
}

周期性状态遥测与数据封装 (CanRecord)

主程序中维护的计时器溢出时,模块主动调用 CanRecord 进行运行状态广播。它将底层的故障代码、状态机状态以及实际采集的电压电流封装成一帧标准报文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/*
** ===================================================================
** Funtion Name : void CanRecord(void)
** Description : 每隔 1S 组包发送模块遥信、遥测参数至上位机
** ===================================================================
*/
void CanRecord(void)
{
unsigned long MailBoxID = 0;
unsigned long data[8]={0}; // 8字节载荷缓存
unsigned long temp=0;

// 1. 组装 29 位发送 ID
MailBoxID = (ECanaMboxes.MBOX0.MSGID.all & 0xF8000000);
MailBoxID = MailBoxID | 0x18000000; // 混入数据页等固定属性

temp = CAN_ADD;
MailBoxID = MailBoxID | temp; // 填入源地址 (模块自身)

temp = MONITOR_ADD;
temp = temp<<8;
MailBoxID = MailBoxID | temp; // 填入目标地址 (上位机)

temp = PF_RECORD_VALUE;
temp = temp<<16;
MailBoxID = MailBoxID | temp; // 填入命令码 (CMD = 0x10)

// 2. 遥信状态转存
data[0] = PFCData.State; // PFC 运行状态
data[1] = PFCData.Err; // PFC 错误码
data[2] = DF.SMFlag; // LLC 状态机 (Init/Wait/Rise/Run/Err)
data[3] = DF.ErrFlag; // LLC 自身硬件保护错误码

// 3. 遥测物理量定点逆变换
// 将底层 DSP 内部 Q 格式的电压采样量转换为真实物理量,并放大 100 倍以整数形式传输。
// 公式:$V_{out} = \frac{V_{avg} \times 7130}{2^{15}}$
temp = SADC.VAvg * 7130 >> 15;
data[4] = temp & 0x00FF; // 电压低 8 位
data[5] = (temp >> 8) & 0x00FF; // 电压高 8 位

// 将底层 DSP 内部 Q 格式的电流采样量转换为真实物理量 (放大100倍)。
// 公式:$I_{out} = \frac{I_{avg} \times 2200}{2^{15}}$
temp = SADC.IAvg * 2200 >> 15;
data[6] = temp & 0x00FF; // 电流低 8 位
data[7] = (temp >> 8) & 0x00FF; // 电流高 8 位

// 4. 装填硬件寄存器并发送
EALLOW;
ECanaRegs.CANME.bit.ME0= 0;
ECanaMboxes.MBOX0.MSGID.all = MailBoxID;

// 使用位移与或运算,将 data[0]~data[3] 合成为一个 32 位的 MDL
ECanaMboxes.MBOX0.MDL.all = (data[0]<<24) | (data[1]<<16) | (data[2]<<8) | data[3];
// 同理,将 data[4]~data[7] 合成为 MDH
ECanaMboxes.MBOX0.MDH.all = (data[4]<<24) | (data[5]<<16) | (data[6]<<8) | data[7];

ECanaRegs.CANME.bit.ME0= 1;
EDIS;

CANTx(); // 触发底层硬件发送
}