此篇记录电压激励从站开发流程

一.先来梳理思路

"STM32 实时将 PDO 数据写入 DAC81408" 的意思是:

STM32 每毫秒从 ET1100 读回主站发的 16 个字节(每 2 字节代表一个 DAC 通道的设定值),把这 16 个字节按照小端序解析成 8 个 16 位数值,然后 自己用另一条 SPI 总线(SPI3)把这 8 个数值分别发给 DAC81408 芯片的 8 个通道,DAC81408 再把这 8 个数值转换成 8 路模拟电压输出。

第一步:DAC81408 是什么?怎么用 SPI 控制它?

DAC81408 是一颗 8 通道 16-bit 精密 DAC 芯片。它有 8 个模拟输出引脚,每个引脚可以输出一个电压值。

它的 SPI 通信协议很简单:

STM32 每次要控制一个通道,就通过 SPI3 发送 2 个字节给 DAC81408:

  字节1: [1][A2][A1][A0][D11][D10][D9][D8]
  字节2: [D7][D6][D5][D4][D3][D2][D1][D0]

  说明:
    [1] = 1(表示"写数据"命令,DAC81408 协议规定 bit7=1 为写)
    [A2][A1][A0] = 通道号 0~7(000=通道0, 001=通道1, ..., 111=通道7)
    [D11~D8] = 16位DAC值的高4位
    [D7~D0] = 16位DAC值的低8位

举例:要设置通道 0 输出 2.5V

假设参考电压 Vref = 5V,全范围 0~5V:

目标电压: 2.5V
DAC_code = 2.5 / 5.0 × 65535 = 32768 = 0x8000

拆分成两个字节:
  字节1 = 0x80 | 0x00 = 0x80
          ↑     ↑
          |     └── 通道0的A2A1A0 = 000
          └── 写命令bit7 = 1
  字节2 = 0x00

STM32 通过 SPI3 发送: 0x80, 0x00
DAC81408 收到后: 通道0输出 2.5V

第二步:主站发来的 PDO 数据到底是什么?

主站(比如 TwinCAT)在 PDO 里发的数据就是 原始字节序列。

主站 PDO 配置(迁移后的新设计):

字节偏移 内容 说明

Byte 0-1

DAC0 设定值

低字节在前(little-endian)

Byte 2-3

DAC1 设定值

低字节在前

Byte 4-5

DAC2 设定值

低字节在前

...

...

...

Byte 14-15

DAC7 设定值

低字节在前

所以主站每毫秒发送一个 EtherCAT 帧,里面有 16 个字节:

例子:主站想设置 通道0=2.5V, 通道1=1.0V, 通道2=3.3V ...

换算成 DAC_code:
  通道0: 2.5/5.0×65535 = 32768 = 0x8000 → 字节 [0x00, 0x80]
  通道1: 1.0/5.0×65535 = 13107 = 0x3333 → 字节 [0x33, 0x33]
  通道2: 3.3/5.0×65535 = 43254 = 0xA8D6 → 字节 [0xD6, 0xA8]

主站发送的 PDO 字节流: [0x00, 0x80, 0x33, 0x33, 0xD6, 0xA8, ...]
                       ↑通道0  ↑通道1  ↑通道2

第三步:STM32 怎么从 ET1100 取数据?

在 PDO_OutputMapping() 中:

// 从 ET1100 SM2 DPRAM 地址读取 16 字节 → 存入 aPdOutputData[]
HW_EscReadIsr(aPdOutputData, 0x1000, 16);

HW_EscReadIsr() 通过 SPI1 发送:

发送: [读命令 0x02] [地址 0x10] [地址 0x00] [4个哑字节]
收到: [回传] [ESC数据 Byte0] [Byte1] ... [Byte15]

读取后 aPdOutputData[] 数组内容就是主站发来的那 16 个字节:

aPdOutputData[0] = 0x00      (DAC0 低字节)
aPdOutputData[1] = 0x80      (DAC0 高字节)
aPdOutputData[2] = 0x33      (DAC1 低字节)
aPdOutputData[3] = 0x33      (DAC1 高字节)
aPdOutputData[4] = 0xD6      (DAC2 低字节)
aPdOutputData[5] = 0xA8      (DAC2 高字节)
...

第四步:APPL_OutputMapping 怎么把字节变成 DAC 结构体?

void APPL_OutputMapping(UINT16* pData)
{
    // pData 指向 aPdOutputData[],里面的数据就是主站发来的原始字节
    // STM32 是小端,所以 UINT16* 强制转换后:
    //   *(pData + 0) = 0x8000   → 通道0的DAC值(字节0=0x00为低,字节1=0x80为高)
    //   *(pData + 1) = 0x3333   → 通道1的DAC值
    //   *(pData + 2) = 0xA8D6   → 通道2的DAC值

    DACOutputs0x7020.DAC0 = *(pData + 0);  // = 0x8000 = 32768
    DACOutputs0x7020.DAC1 = *(pData + 1);  // = 0x3333 = 13107
    DACOutputs0x7020.DAC2 = *(pData + 2);  // = 0xA8D6 = 43254
    // ... DAC3 ~ DAC7 同理
}

这里不需要做任何位运算,因为主站发的字节顺序(little-endian)和 STM32 的内存布局完全一致,直接按 16-bit 读出来就是正确的 DAC 数值。

第五步:APPL_Application 把数据发给 DAC81408

void APPL_Application(void)
{
    // 调用自己写的 DAC81408 驱动函数
    DAC81408_SetChannel(0, DACOutputs0x7020.DAC0);  // 发送 0x8000
    DAC81408_SetChannel(1, DACOutputs0x7020.DAC1);  // 发送 0x3333
    DAC81408_SetChannel(2, DACOutputs0x7020.DAC2);  // 发送 0xA8D6
    // ... 全部8通道
    DAC81408_LoadAll();  // 让 DAC 同时更新所有通道输出
}

DAC81408_SetChannel() 内部做的事:

void DAC81408_SetChannel(uint8_t channel, uint16_t value)
{
    // 以 channel=0, value=0x8000 为例:
    uint8_t byte1 = 0x80 | (channel << 4);  // 0x80 | 0x00 = 0x80
    uint8_t byte2 = value & 0xFF;           // 0x8000 & 0xFF = 0x00

    // 通过 SPI3 发送两个字节给 DAC81408
    HAL_SPI_Transmit(&hspi3, &byte1, 1, HAL_MAX_DELAY);
    HAL_SPI_Transmit(&hspi3, &byte2, 1, HAL_MAX_DELAY);
}

HAL_SPI_Transmit(&hspi3, &byte1, 1, ...) 的物理意义:

STM32 作为 SPI3 Master:
  MOSI (PC12) ──────────────→ DAC81408 DIN 引脚
  SCK  (PC10) ──────────────→ DAC81408 SCLK 引脚
  CS   (PC9)  ──────────────→ DAC81408 SYNC 引脚
  
  发送字节 0x80:
    bit7=1 (命令位), bit6-4=000 (通道0), bit3-0=1000 (高4位数据=0x8000的高4位=8)
  
  发送字节 0x00:
    bit7-0 = 0x00 (低8位数据)

全过程图示

时刻: t = 1ms
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

① 主站 TwinCAT 发送 EtherCAT 帧
   PDO 数据: [0x00 0x80] [0x33 0x33] [0xD6 0xA8] [其他通道...]
             ↑通道0      ↑通道1      ↑通道2

② ET1100 芯片接收帧,写入内部 SM2 DPRAM
   地址 0x1000: 0x00
   地址 0x1001: 0x80
   地址 0x1002: 0x33
   ...

③ STM32 SPI1 读取(HW_EscReadIsr)
   SPI1 MOSI → ET1100 MISO
   发送: [读命令 0x02] [0x10] [0x00] [x] [x] [x] [x]
   收到: [0x02] [0x00] [0x80] [0x33] [0x33] [0xD6] ...

④ aPdOutputData[] 数组在 STM32 RAM 中
   [0] = 0x00, [1] = 0x80, [2] = 0x33, [3] = 0x33, ...

⑤ APPL_OutputMapping() 解析
   DAC0 = aPdOutputData[0,1] = 0x8000 = 32768
   DAC1 = aPdOutputData[2,3] = 0x3333 = 13107
   DAC2 = aPdOutputData[4,5] = 0xA8D6 = 43254

⑥ APPL_Application() 写入 DAC81408
   SPI3 MOSI → DAC81408 DIN

   发第1组: [0x80] [0x00]
             ↑通道0 数据=0x8000
             通道0 输出电压 = 32768/65535 × 5V = 2.5V

   发第2组: [0x81] [0x33]   ← 0x81 = 命令1 + 通道1
             ↑通道1 数据=0x3333
             通道1 输出电压 = 13107/65535 × 5V = 1.0V

   发第3组: [0x82] [0xD6]   ← 0x82 = 命令2 + 通道2
             ↑通道2 数据=0xA8D6
             通道2 输出电压 = 43254/65535 × 5V = 3.3V

   发 LoadAll 命令: [0x24]
             所有通道同时更新输出电压

⑦ DAC81408 8个输出引脚
   OUT0 = 2.5V, OUT1 = 1.0V, OUT2 = 3.3V, ...

二.协议栈和对象字典

硬件变更总览

变更项 原配置 新配置

MCU

STM32F407ZET6 (F4)

STM32H755IIK3 (H7)

ET1100 SPI

SPI1 (PB3/4/5, CS=PE2)

SPI1 保持不变

DAC81408

新增 SPI3

DI 开关

GPIOC PC8-PC15 (8路)

移除

DO LED

GPIOG PG8-PG15 (8路)

移除

协议栈大部分不变,需要改的是对象字典,两种方案
a.在上个工程的协议栈基础上改自定义对象字典的文件,改XML

b.重新生成SSC,在Excel里改对象字典

1.SSC配置

与上一个文章生成的一致,仅需更改DEVICE_NAME来进行区分(还是贴图放一下,以便参考)

下面这一页按照前面的改好后再次打开选项变少

创建新的excel对象字典

指标要求:

8通道电压激励输出,且可独立配置使能或关闭,激励电压可配置+5V和+10V

需求转换设计思路:

每个 DAC 通道需要两个可独立配置的参数:

  • 使能(EN):0 = 关闭,1 = 使能
  • 电压等级(VS):00 = 保留,01 = +5V,10 = +10V

每个通道 3 bits,8 个通道共 24 bits,PDO 占用 3 字节。

8通道独立使能/关闭 + 电压可配 + 仅 RxPDO 无 TxPDO

接下来写对象字典
1. 通道对象 0x7000 ~ 0x7007(8个DAC通道)

只需要 1 个对象 + 3 个子索引 × 8 个通道:

每个通道用 Record 类型,包含 3 个子索引:

SSC 工具自动为每个 RECORD 对象添加 SubIndex0,在 Excel 里再写一行 SI=0 就重复了。

所以不用考虑0x6000 和 0x7010 中的 SubIndex0 行:

这里要注意一点,四字节对齐;当前设计的问题是 BOOLEAN 在 PDO 中只占 1 bit,导致实际 PDO 布局不对齐:

将每个通道的 Channel Enable 从 BOOLEAN 改为 UNSIGNED8:

修正后的 PDO 布局

每个通道 = UNSIGNED16(2B) + UNSIGNED8(1B) + UNSIGNED8(1B) = 4字节 ✅

字节布局:
  Byte 0-1:  通道0 DAC Code
  Byte 2:    通道0 Enable (bit0=使能位,bit1-7=保留)
  Byte 3:    通道0 Voltage Range
  Byte 4-5:  通道1 DAC Code
  Byte 6:    通道1 Enable
  Byte 7:    通道1 Voltage Range
  ...
  Byte 31:   通道7 Voltage Range

8通道 × 4字节 = 32字节(4字节对齐)✅

总结来说就是 把每个通道的 SI2: BOOLEAN 改成 SI2: UNSIGNED8,firmware 中只取 bit0 作为使能位,其余 bit 填 0 保留。这样每个通道刚好占 4 字节,8 通道共 32 字节,完全 4 字节对齐。

保存后改名mydacapp

创建协议栈,(突然变小......

三.cubeMX生成32基础工程

此过程中,我们需要开启以下引脚功能:

  • 外部晶振;
  • SPI1,SPI3;
  • 1 个定时器中断;
  • 3 个外部中断(一个主中断,两个时钟同步中断,该程序使用的是 DC 同步,如果是用 Free 同步或者是 SM 同步,则只需要开一个主中断,主中断和定时器中断的优先级一样且要高于时钟同步中断);
  • 所使用的的 I/O 引脚。
  • 串口(非必须)。

方案 :M7 单独跑 EtherCAT,M4 空闲(先按照流程走)

1.创建工程

选择型号后出现这个弹窗

STM32CubeMX 针对 Cortex-M7 内核 MCU 的 MPU 配置推荐提示

  • 你选了一颗 Cortex-M7 内核的 STM32(比如 H7、F7 系列),CubeMX 主动提醒你配置 MPU。
  • 目的是避免 Cortex-M7 的 “推测执行” 特性导致的内存访问异常,提升系统稳定性。
选项 效果 适用场景
Yes CubeMX 自动生成一套默认 MPU 配置:1. 把 4GB 地址空间设为一个默认 Region,允许访问2. 禁用了对外部内存的推测读取风险3. 生成的代码会在系统初始化时自动配置 MPU 绝大多数新手 / 普通项目,直接用默认配置就能避免大部分 MPU 相关的 HardFault
No 不自动配置 MPU,你需要自己在代码里手动初始化(或者完全不用 MPU,等于所有内存区域都开放权限) 你需要完全自定义 MPU 规则,或者项目中没有缓存 / 外部内存,对稳定性要求不高

点YES

A.时钟

点击RCC→选择外部时钟,选择crystal/ceramic resonator

B.系统debug

Pinout & Configuration有下拉选项trace and debug

点击SYS→DEBUG选择serial wire

C.SPI1/SPI3

所在总线

外设 连接的SPI 最大SPI时钟 需要的时钟

ET1100

SPI1

≤10MHz

10MHz

DAC81408

SPI3

≤50MHz

20MHz(常用推荐值)

两者共用同一个 PLL1_P 时钟源,但分频系数必须不同,这是一个非常合理的 H7 设计。

目标:PLL1_P 输出同时满足两个 SPI 的时钟需求。

第一步:选 DIVM1(输入分频)

25MHz 输入,建议选 DIVM1 = 5:

25MHz / 5 = 5MHz → 进入 VCO

第二步:选 DIVN1(倍频)

VCO 需要在 200~800MHz 之间,选 DIVN1 = 160:

5MHz × 160 = 800MHz → VCO 频率 = 800MHz ✓

第三步:选 DIVP1(PLL1_P 分频)

800MHz 输出太大,需要分频,选 DIVP1 = 4:

800MHz / 4 = 200MHz → PLL1_P = 200MHz

验证:200MHz 在 PLL1_P 的可用范围内(H7 的 PLL1_P 支持 1.95~800MHz),完全正常。

开始配置:选好引脚进行配置,注意NSS进行软件配置

a.SPI1-ET1100

GPIO 配置(片选)
点击 片选引脚 → 选 GPIO_Output,然后在 GPIO 页配置:

b.SPI3-DAC81408

读DAC81408的手册,

参数 说明

最大 SCLK

50MHz(VIO=2.7V~5.5V)

写入最大频率

SCLK 高/低时间

≥10ns

50MHz 时各 10ns

SDI 建立时间

≥5ns

数据在 SCLK 下降沿前到位

SDI 保持时间

≥5ns

数据在 SCLK 下降沿后保持

CS↓ 到 SCLK↓

≥15ns

片选到第一个时钟的建立时间

数据帧长度

24bit

1bit(R/W) + 1bit(x) + 6bit(地址) + 16bit(数据)

数据格式

MSB First

高位先发

SDI 采样边沿

SCLK 下降沿

SDI 在下降沿被 DAC 锁存

SDO 输出边沿

SCLK 上升沿(默认 FSDO=0)

默认模式下 MOSI 和 MISO 不冲突

DAC81408 的帧是 24bit,但 STM32 SPI 每次固定 8bit 发送,需要分 3 次发完(高字节→中字节→低字节)

  • 采样沿:SCLK 下降沿采 SDI
  • 输出沿:SDO 在 SCLK 上升沿输出(默认 FSDO=0)
  • 模式:CPOL=0,CPHA=1(SPI Mode 1)

GPIO 配置(片选)
点击 片选引脚 → 选 GPIO_Output,然后在 GPIO 页配置:(这一页要注意,GPIO output是低不是高,后面几页截图有误,按此页

D.EXTI 中断配置(3路)

先在pin选好引脚,然后分别在GPIO中配置:

a.PA8 — AL Event 中断

b.PC0 — DC Sync0 中断

c.PC1 — DC Sync1 中断

E.TIM6 配置(EtherCAT 心跳定时器)

生成1ms时钟

APB1 Timer Clock = 100MHz(APB2=50MHz,×2=100MHz)

Prescaler (PSC):     200-1    ← 100MHz / 200 = 500kHz
Counter Period (ARR): 500-1    ← 500kHz / 500 = 1000Hz = 1ms

→ Update Event = 1ms  ✓

勾选NVIC中断使能

F.中断优先级设置

左侧 System Core → NVIC,设置各中断优先级:

CubeMX 中优先级组选 4 bits for pre-emption / 0 bits for subpriority 不合适,因为参考工程用的是 Group_1(1位抢占+3位子)。

CubeMX NVIC 设置:

System Core → NVIC → Priority Group 选择:1 bit for pre-emption priority, 3 bits for subpriority

然后各中断设置:

TIM6与 Sync0/Sync1 相同,确保 AL Event(抢占0)仍是最高优先级,三者都低于它即可。

2.Project Manager 设置

GENERATE code

四.协议栈移植

1.keil下载芯片包

无语了在线下离线下都不成功,离线在官网下载了好几个版本,4.版本的都不行,keil版本老,3.1.0终于成功

2.加入协议栈代码到STM32H755工程中

添加编译路径

编译后报错和警告一堆:

不用慌,接下来慢慢移植

3.移植

SSC 生成的代码分为三层:应用层 (mydacapp.c)、协议栈通用层 (ecatslv.c 等) 和硬件层 (el9800hw.c)。

A.el9800hw.c文件

协议栈底层还在尝试访问 PIC 单片机的引脚和 SPI 控制器,导致 STM32 编译器完全无法识别。

  • 根据报错去添加define(由于没有用到SYNC1,所以SYNC1的定义不做任何处理)
  • 添加spi、timer和exit初始化函数
  • 修改RxTxSpiData函数,改成SPI先发送后接收函数
  • 增加定时器中断处理函数
a.先修改头文件包含:
#include "ecat_def.h"

#include "spi.h"
#include "gpio.h"
#include "tim.h"

#include "ecatslv.h"


#define    _EL9800HW_ 1
#include "el9800hw.h"
#undef    _EL9800HW_
/*remove definition of _EL9800HW_ (#ifdef is used in el9800hw.h)*/

#include "ecatappl.h"

#include "main.h"
b.添加SPI 相关宏定义:

(添加片选和 SPI 收发核心宏)

/*-----------------------------------------------------------------------------------------
------
------    SPI defines/macros
------
-----------------------------------------------------------------------------------------*/

#define SPI_DEACTIVE                    1
#define SPI_ACTIVE                        0
 
// ====================== 你需要添加的部分 ======================
// SPI片选控制(PB0,根据你的实际接线修改)
#define SELECT_SPI    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET)
#define DESELECT_SPI  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET)
 
// SPI单字节收发核心宏(所有SPI读写都通过这个宏)
// HAL_SPI_TransmitReceive:发送一个字节,同时接收一个字节
extern SPI_HandleTypeDef hspi1;  // 声明CubeMX生成的SPI句柄
extern TIM_HandleTypeDef htim6;  // 添加这一行,声明TIM6句柄
static inline uint8_t WR_CMD(uint8_t data)
{
    uint8_t tx = data;
    uint8_t rx = 0;
    HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, HAL_MAX_DELAY);
    return rx;
}
 
// 防止编译器优化的volatile宏
#define VARVOLATILE volatile
// =============================================================

为什么这么改?

  • 官方代码的SELECT_SPI和DESELECT_SPI原来在SPI1.h里,现在我们直接定义在这里
  • WR_CMD是整个文件最核心的宏,所有 SPI 读写都通过它实现,替换了原来标准库的 SPI 函数
  • 声明hspi1是因为 CubeMX 生成的 SPI 句柄在spi.c里,这里需要外部引用
     
c.添加全局中断宏
/*-----------------------------------------------------------------------------------------
------
------    Global Interrupt setting
------
-----------------------------------------------------------------------------------------*/

#define DISABLE_GLOBAL_INT           		   __disable_irq()			
#define ENABLE_GLOBAL_INT           		    __enable_irq()					
#define    DISABLE_AL_EVENT_INT           DISABLE_GLOBAL_INT
#define    ENABLE_AL_EVENT_INT            ENABLE_GLOBAL_INT

说明__disable_irq()__enable_irq()是 CMSIS 标准函数,HAL 库和标准库都能用。

d.ESC 主中断(AL_EVENT)宏定义
/*-----------------------------------------------------------------------------------------
------
------    ESC Interrupt
------
-----------------------------------------------------------------------------------------*/
#if AL_EVENT_ENABLED
// ====================== 适配 PA8=IRQ中断 ======================
#define    INIT_ESC_INT           /* CubeMX已经初始化EXTI,这里空着 */
#define    EcatIsr                EXTI9_5_IRQHandler  // PA8对应EXTI线8,归入EXTI9_5共用向量
#define    ACK_ESC_INT            __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_8);  // 清除PA8的EXTI中断标志
// =============================================================
#endif
e.SYNC0/SYNC1 中断宏定义
/*-----------------------------------------------------------------------------------------
------
------    SYNC0 Interrupt
------
-----------------------------------------------------------------------------------------*/
#if DC_SUPPORTED
// ====================== 适配PC0=SYNC0、PC1=SYNC1(STM32H7正确向量) ======================
// SYNC0:PC0 → EXTI线0 → 独立向量EXTI0_IRQn
#define    INIT_SYNC0_INT                  /* CubeMX已经初始化EXTI,这里空着 */		
#define    Sync0Isr                        				EXTI0_IRQHandler
#define    DISABLE_SYNC0_INT             NVIC_DisableIRQ(EXTI0_IRQn);
#define    ENABLE_SYNC0_INT               NVIC_EnableIRQ(EXTI0_IRQn);
#define    ACK_SYNC0_INT                  __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
 
// SYNC1:PC1 → EXTI线1 → 独立向量EXTI1_IRQn
#define    INIT_SYNC1_INT                  /* CubeMX已经初始化EXTI,这里空着 */
#define    Sync1Isr                        						EXTI1_IRQHandler
#define    DISABLE_SYNC1_INT                 NVIC_DisableIRQ(EXTI1_IRQn);
#define    ENABLE_SYNC1_INT                 NVIC_EnableIRQ(EXTI1_IRQn); 
#define    ACK_SYNC1_INT                  	 __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_1);
// =============================================================
#endif
f.硬件定时器宏定义
/*-----------------------------------------------------------------------------------------
------
------    Hardware timer
------
-----------------------------------------------------------------------------------------*/
// 只保留ECAT_TIMER_INT判断,这是SSC官方定义的宏
#if ECAT_TIMER_INT
#define ECAT_TIMER_INT_STATE
// ====================== 适配TIM6定时器 ======================
#define ECAT_TIMER_ACK_INT         __HAL_TIM_CLEAR_IT(&htim6, TIM_IT_UPDATE);
#define    TimerIsr               TIM6_DAC_IRQHandler  // TIM6的中断向量名(H7系列)
#define    ENABLE_ECAT_TIMER_INT   HAL_TIM_Base_Start_IT(&htim6);
#define    DISABLE_ECAT_TIMER_INT  HAL_TIM_Base_Stop_IT(&htim6);

#define INIT_ECAT_TIMER            /* CubeMX已经初始化TIM6 */

#define STOP_ECAT_TIMER           DISABLE_ECAT_TIMER_INT;

#define START_ECAT_TIMER         ENABLE_ECAT_TIMER_INT
// =============================================================

#else    //#if ECAT_TIMER_INT

// ====================== 适配TIM6定时器(无中断) ======================
#define INIT_ECAT_TIMER           /* CubeMX已经初始化 */
#define STOP_ECAT_TIMER           HAL_TIM_Base_Stop(&htim6);
#define START_ECAT_TIMER          HAL_TIM_Base_Start(&htim6);
// =============================================================

#endif //#else #if ECAT_TIMER_INT
g.完全重写 RxTxSpiData 函数(核心中的核心)
  • 原代码是 PIC 单片机的 SPI 寄存器操作,在 STM32 上完全无效
  • 我们已经定义了WR_CMD宏,封装了 HAL 库的HAL_SPI_TransmitReceive函数
  • 这样既保留了 SSC5.12 官方改进的 SPI 访问结构,又适配了 STM32 硬件
static UINT8 RxTxSpiData(UINT8 MosiByte)
{
    // 直接使用我们定义的WR_CMD宏,所有SPI收发都通过HAL库实现
    return WR_CMD(MosiByte);
}

AddressingEsc 函数(不用管)

  • 官方设计是AddressingEsc只负责拉低点选和发送地址
  • 拉高点选的操作在HW_EscReadHW_EscWrite函数里
  • 这是 SSC5.12 的改进,目的是减少 SPI 传输的开销
h.重写 HW_Init 函数

(删除所有 PIC 初始化代码)

UINT8 HW_Init(void)
{
    UINT32 intMask;
 
    // ====================== 删除所有PIC单片机专属初始化代码 ======================
    // PORT_CFG;          // PIC端口配置寄存器操作,STM32无效
    // SPI1_CON1 = ...;   // PIC SPI控制寄存器操作,STM32无效
    // SPI1_STAT = ...;   // PIC SPI状态寄存器操作,STM32无效
    // (_INT1IE)=1;       // PIC中断使能寄存器操作,STM32无效
 
    // ====================== 官方保留:ESC通信可靠性测试(必须保留!) ======================
    // 向AL Event Mask寄存器(0x204)写入0x93,然后读回验证
    // 这是判断SPI通信是否正常、ET1100是否正常工作的唯一标准
    do
    {
        intMask = 0x93;
        HW_EscWriteDWord(intMask, ESC_AL_EVENTMASK_OFFSET);
        intMask = 0;
        HW_EscReadDWord(intMask, ESC_AL_EVENTMASK_OFFSET);
    } while (intMask != 0x93);
 
    // 测试通过,关闭所有中断掩码,等待协议栈开启需要的中断
    intMask = 0x00;
    HW_EscWriteDWord(intMask, ESC_AL_EVENTMASK_OFFSET);
 
    // ====================== 初始化中断和定时器(保留官方结构) ======================
    INIT_ESC_INT;       // 空宏,EXTI已在CubeMX中初始化
 
    INIT_SYNC0_INT;     // 空宏,EXTI已在CubeMX中初始化
    INIT_SYNC1_INT;     // 空宏,EXTI已在CubeMX中初始化
 
    ENABLE_SYNC0_INT;   // 使能SYNC0中断
    ENABLE_SYNC1_INT;   // 使能SYNC1中断
 
    INIT_ECAT_TIMER;    // 空宏,TIM6已在CubeMX中初始化
    START_ECAT_TIMER;   // 启动TIM6定时器中断
 
    // 使能全局中断
    ENABLE_GLOBAL_INT;
 
    return 0;
}
i.修改 HW_EscRead 和 HW_EscWrite 函数

1.所有出现DESELECT_SPI的地方都要加分号,包括:

  • HW_EscRead函数里的DESELECT_SPI
  • HW_EscReadIsr函数里的DESELECT_SPI
  • HW_EscWrite函数里的DESELECT_SPI
  • HW_EscWriteIsr函数里的DESELECT_SPI
     

为什么?

  • 你定义的DESELECT_SPI是一个宏,展开后是HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
  • 这是一个 C 语句,必须以分号结尾,否则会导致编译错误

2.替换所有 Nop() 为 __NOP()

所有出现Nop()的地方都要替换,包括:

  • HW_EscRead函数里的 10 个Nop()
  • HW_EscReadIsr函数里的 10 个Nop()
  • HW_EscWrite函数里的 10 个Nop()
  • HW_EscWriteIsr函数里的 10 个Nop()

为什么?

  • Nop()是 PIC 单片机的空操作函数
  • STM32 HAL 库的空操作函数是__NOP(),定义在core_cm4.h里

j.重写所有中断服务函数

(删除 PIC 中断属性改为HAL)

原代码(PIC 版)

/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Interrupt service routine for the PDI interrupt from the EtherCAT Slave Controller
*////////////////////////////////////////////////////////////////////////////////////////

void __attribute__ ((__interrupt__, no_auto_psv)) EscIsr(void)
{
     PDI_Isr();

    /* reset the interrupt flag */
    ACK_ESC_INT;

}



/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Interrupt service routine for the interrupts from SYNC0
*////////////////////////////////////////////////////////////////////////////////////////
void __attribute__((__interrupt__, no_auto_psv)) Sync0Isr(void)
{
    Sync0_Isr();

    /* reset the interrupt flag */
    ACK_SYNC0_INT;
}
/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Interrupt service routine for the interrupts from SYNC1
*////////////////////////////////////////////////////////////////////////////////////////
void __attribute__((__interrupt__, no_auto_psv)) Sync1Isr(void)
{
    Sync1_Isr();

    /* reset the interrupt flag */
    ACK_SYNC1_INT;
}

修改后(STM32 HAL 版)

/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Interrupt service routine for the PDI interrupt from the EtherCAT Slave Controller
*////////////////////////////////////////////////////////////////////////////////////////



// PA8对应EXTI9_5独立中断向量
void EXTI9_5_IRQHandler(void)
{
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_8) != RESET)
    {
        PDI_Isr();  // 协议栈PDI中断处理(核心!所有ESC事件都在这里处理)
        ACK_ESC_INT;  // 清除PE3的EXTI中断标志
    }
}


/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Interrupt service routine for the interrupts from SYNC0(PC0)
*////////////////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Interrupt service routine for the interrupts from SYNC1(PC1)
*////////////////////////////////////////////////////////////////////////////////////////


#if DC_SUPPORTED
void EXTI0_IRQHandler(void)
{
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
    {
        Sync0_Isr();
        ACK_SYNC0_INT;
    }
}

void EXTI1_IRQHandler(void)
{
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_1) != RESET)
    {
        Sync1_Isr();
        ACK_SYNC1_INT;
    }
}
#endif

/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    Timer interrupt service routine (1ms)
*////////////////////////////////////////////////////////////////////////////////////////
// TIM6的中断服务函数(和DAC1中断共用向量)

void TIM6_DAC_IRQHandler(void)
{
    if(__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE) != RESET)
    {
        DISABLE_ESC_INT();  // 替换为官方定义的宏
				ECAT_CheckTimer();  // 协议栈核心定时器函数:处理看门狗和状态机超时
				ECAT_TIMER_ACK_INT;
				ENABLE_ESC_INT();  // 替换为官方定义的宏
    }
}

/** @} */

Rebuild-此时报错少很多

el9800hw.c中还有两处警告,放心,更改el9800hw.h后消除

B.el9800hw.h文件

a.增加stm32h7xx.h&main.h头文件包含
/*-----------------------------------------------------------------------------------------
------
------    Includes
------
-----------------------------------------------------------------------------------------*/
#include  "esc.h"


// (标准库头文件,HAL库工程不需要)
#include "stm32h7xx.h"
 
// 正确写法(CubeMX生成的main.h包含所有HAL库头文件)
#include "main.h"
b.位操作宏部分

/*---------------------------------------------
-    Microcontroller definitions
-----------------------------------------------*/
// 所有GPIO操作都在myapp.c中使用HAL库函数实现,不需要这些标准库位操作宏
c.定时器设置部分

这个宏定义的是 TIM 每毫秒的计数增量,用于 EtherCAT 协议栈计算超时和看门狗时间基准。

TIM6挂载在APB1(50M)上,timer为50*2=100M,前面配置PSC=200、ARR=500,那么100M/200=0.5MHz,0.5M/500=1kHz;

所以要想得到1ms(1khz)计时,那么需要ECAT_TIMER_INC_P_MS 必须是 500。

添加:

/*---------------------------------------------
-    hardware timer settings
-----------------------------------------------*/

#define ECAT_TIMER_INC_P_MS                500 /**< \brief 每毫秒定时器计数增量(TIM6=0.5MHz计数频率)*/
d.中断和定时器宏定义部分

添加:

/*---------------------------------------------
-    Interrupt and Timer defines
-----------------------------------------------*/

#ifndef DISABLE_ESC_INT
#define    DISABLE_ESC_INT()      NVIC_DisableIRQ(EXTI9_5_IRQn)   // 禁用PA8(EXTI8)中断
#endif
#ifndef ENABLE_ESC_INT
#define    ENABLE_ESC_INT()       NVIC_EnableIRQ(EXTI9_5_IRQn)    // 使能PA8(EXTI8)中断
#endif

#ifndef HW_GetTimer
#define HW_GetTimer()            TIM6->CNT      // 读取TIM6计数器当前值
#endif

#ifndef HW_ClearTimer
#define HW_ClearTimer()          ((TIM6->CNT)=0);  // 清零TIM6计数器
#endif

后续为ESC 访问宏定义部分(完全保留,不需要修改

说明

  • 这些宏是 SSC 官方要求的,是对HW_EscReadHW_EscWrite函数的封装
  • 协议栈内部大量使用这些宏,绝对不能修改或删除

函数声明部分

不用改,SSC5.12已经做好了

C.ecat_def.h文件

/** 
OBJ_DWORD_ALIGN: Shall be set if the object structures are not Byte aligned and 32bit entries are implicitly padded to even 32bit memory addresses. */
#ifndef OBJ_DWORD_ALIGN
#define OBJ_DWORD_ALIGN                           1
#endif

/** 
OBJ_WORD_ALIGN: Shall be set if the object structures are not Byte aligned and 16bit entries are implicitly padded to even 16bit memory addresses. */
#ifndef OBJ_WORD_ALIGN
#define OBJ_WORD_ALIGN                            0
#endif

该头文件定义如果为0,myapp中APPL_GenerateMapping中数据大小计算会出错

D.mydacapp.c

要使用mydacapp.c底部的主函数,需要:

  • 删除 CubeMX 生成的main.c文件,或者删除main.c中的main()函数
  • 确保mydacapp.c中的main()函数包含以下内容:

需要修改mydacapp.c文件:

  • 修改APPL_InputMapping函数(对于主站来说是输入,对于STM32来说是输出)
  • 修改APPL_OutputMapping函数(对于主站来说是输出,对于STM32来说是输入)
  • 修改APPL_Application函数,在该函数中对输入输出数据进行处理(将写的值输出出去)
  • 修改main函数,给写入主站的值赋初值
  • 其他APPL函数是在起始/结束某个行为时调用的函数,如果有需要,也可以修改
a.头文件修改

需要包含el9800hw.hmain.h来使用 HAL 库函数和 GPIO 定义。

/*-----------------------------------------------------------------------------------------
------
------    Includes
------
-----------------------------------------------------------------------------------------*/
#include "ecat_def.h"

#include "applInterface.h"


#include "el9800hw.h"  // 添加硬件抽象层头文件
#include "main.h"       // 添加CubeMX生成的头文件
#include "spi.h"       //需要用SPI3进行DAC81408的访问

#define _MYDACAPP_ 1
#include "mydacapp.h"
#undef _MYDACAPP_
b.状态转换回调函数部分
  • 这是 EtherCAT 从站最重要的安全功能
  • 当主站断开连接或从站发生错误时,会自动调用这个函数
  • 必须在这里复位所有输出,防止设备保持在危险状态
  • 其他状态转换回调函数(APPL_AckErrorIndAPPL_StartMailboxHandler等)保持原样即可

将原本的 GPIO 输出改为 SPI3 控制 DAC81408 

        核心思路

退出 OP 状态时,将所有 DAC 通道复位到安全值(通常是 0,即 0V 输出)。根据 DAC81408 的手册,复位数据寄存器即可实现。

改动要点:

  • tx[0] = 0x00 | (0x14 + ch) — 即写地址 + 通道号
  • tx[1] / tx[2] = 0x00 — 数据清零
  • 传输 3 字节(24-bit 完整帧)
  • 片选使用 PB2,与顶部宏一致
/////////////////////////////////////////////////////////////////////////////////////////
/**
 \return     0, NOERROR_INWORK

 \brief    The function is called in the state transition from OP to SAFEOP
             to stop the output handler. This functions informs the application
             about the state transition, the application cannot refuse
             the state transition.

*////////////////////////////////////////////////////////////////////////////////////////

UINT16 APPL_StopOutputHandler(void)
{
		// 当从站退出OP状态时,复位所有DAC通道输出到0V(安全状态)
    uint8_t tx[3], rx[3];
    for (uint8_t ch = 0; ch < 8; ch++)
    {
        // 发送 0x0000 到通道 ch,使其输出 0V
        tx[0] = 0x00 | (DAC_REG_DAC0 + ch);   // Bit[23]=0(写) + 6位寄存器地址
        tx[1] = 0x00;                          // 数据高字节
        tx[2] = 0x00;                          // 数据低字节
        DAC81408_CS_LOW();
        HAL_SPI_TransmitReceive(&hspi3, tx, rx, 3, HAL_MAX_DELAY);
        DAC81408_CS_HIGH();  // CS上升沿触发异步更新
    }

    
    return ALSTATUSCODE_NOERROR;
}

PDO 映射生成函数APPL_GenerateMapping保持原样

c.输入映射函数(从站→主站)

无回传数据,无需更改

  • 这个函数负责把本地输入变量sDIInputs的值拷贝到 ESC 的输入 PDO 缓冲区
/////////////////////////////////////////////////////////////////////////////////////////
/**
\param      pData  pointer to input process data

\brief      This function will copies the inputs from the local memory to the ESC memory
            to the hardware
*////////////////////////////////////////////////////////////////////////////////////////
void APPL_InputMapping(UINT16* pData)
{
#if _WIN32
   #pragma message ("Warning: Implement input (Slave -> Master) mapping")
#else
    #warning "Implement input (Slave -> Master) mapping"
#endif
}
d.输出映射函数(主站→从站)
  • 这个函数负责把 ESC 的输出 PDO 缓冲区的值拷贝到本地输出变量sDOOutputs

根据我们的对象字典(8通道 DAC,每通道:DAC Code 16bit + Channel Enable 8bit + Voltage Range 8bit),pData 缓冲区布局如下:

pData[0] → DAC0 Code (16bit,低字节在前)
pData[1] → DAC0 Channel Enable (8bit) + Voltage Range (8bit)
pData[2] → DAC1 Code (16bit)
pData[3] → DAC1 Channel Enable (8bit) + Voltage Range (8bit)
...
pData[14] → DAC7 Code (16bit)
pData[15] → DAC7 Channel Enable (8bit) + Voltage Range (8bit)
通道 ch 的 PDO 数据布局(按字节):
  Byte[0] = DAC Code 低字节
  Byte[1] = DAC Code 高字节
  Byte[2] = Channel Enable (8 bit)
  Byte[3] = Voltage Range (8 bit)

实现代码

需要以 字节 为单位读取 PDO 数据,而不是用 UINT16 数组下标去猜位置。因为每个通道只占 3 个字节 + 1 字节 padding,UINT16 下标会出现重叠

/////////////////////////////////////////////////////////////////////////////////////////
/**
\param      pData  pointer to output process data

\brief    This function will copies the outputs from the ESC memory to the local memory
            to the hardware
*////////////////////////////////////////////////////////////////////////////////////////
//将主站 PDO 数据写入 DAC81408

void APPL_OutputMapping(UINT16* pData)
{
		// 禁用EtherCAT中断,避免与SPI3操作冲突
    DISABLE_ESC_INT();
    for (uint8_t ch = 0; ch < 8; ch++)
    {
        // 以字节为单位读取 PDO 数据(避免 UINT16 下标重叠问题)
        uint8_t* pByte = (uint8_t*)pData;
        uint8_t  offset = ch * 4;   // 每个通道占 4 字节(32 bit = 3 字节数据 + 1 字节 padding)
        // PDO 数据排列(小端序):
        // pByte[offset + 0] = DAC Code 低字节
        // pByte[offset + 1] = DAC Code 高字节
        // pByte[offset + 2] = Channel Enable (8 bit)
        // pByte[offset + 3] = Voltage Range (8 bit)
        uint16_t dacCode = (uint16_t)pByte[offset + 1] << 8 | pByte[offset + 0];
        uint8_t  enable  = pByte[offset + 2];
        uint8_t  vRange  = pByte[offset + 3];
        DAC81408_WriteChannel(ch, dacCode, enable, vRange);
    }
    ENABLE_ESC_INT();
}

添加函数DAC81408_WriteChannel 的具体实现如下(参考常见 DAC81408 SPI 协议):

这个结构体要放在调用之前

添加片选引脚、电压范围配置相关的地址定义:

// DAC81408 片选(PB2),在文件顶部定义
#define DAC81408_CS_LOW()   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET)
#define DAC81408_CS_HIGH()  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET)

// DAC81408 SPI 寄存器地址
#define DAC_REG_DAC0          0x14   // DAC0 数据寄存器地址
#define DAC_REG_GENCONFIG     0x04   // 通用配置寄存器
#define DAC_REG_DACRANGE_L    0x0C   // DAC[3:0] 电压范围寄存器
#define DAC_REG_DACRANGE_H    0x0B   // DAC[7:4] 电压范围寄存器
#define DAC_REG_SPICONFIG     0x03   // SPI 配置寄存器
#define DAC_REG_TRIGGER       0x0E   // 触发寄存器
/////////////////////////////////////////////////////////////////////////////////////////
// DAC81408_WriteChannel
// ch:       通道号 0~7
// dacCode:  16-bit DAC代码 (0x0000 ~ 0xFFFF)
// enable:   1=使能,0=禁用(禁用时输出0V)
// vRange:   电压范围 (见 DAC81408 datasheet Table 19)
//            0x00 = 0 to 5V
//            0x01 = 0 to 10V
//            0x02 = 0 to 20V
//            0x04 = 0 to 40V
//            0x09 = -5 to +5V
//            0x0A = -10 to +10V
//            0x0C = -20 to +20V
//            0x0E = -2.5 to +2.5V
/////////////////////////////////////////////////////////////////////////////////////////
static void DAC81408_WriteChannel(uint8_t ch, uint16_t dacCode, uint8_t enable, uint8_t vRange)
{
    uint8_t tx[3], rx[3];
    // 通道未使能时强制输出0V
    if (!enable) {
        dacCode = 0x0000;
    }
		
    // ---- 1. 写DAC数据寄存器 (24-bit SPI 完整帧) ----
    // Bit[23]   = 0 (写操作)
    // Bit[22]   = 0 (忽略)
    // Bit[21:16]= 6位寄存器地址 (DAC0=0x14, DAC1=0x15, ...)
    // Bit[15:0] = 16位DAC数据 (MSB在前)
    tx[0] = 0x00 | (DAC_REG_DAC0 + ch);          // 写命令 + 通道寄存器地址
    tx[1] = (uint8_t)(dacCode >> 8);              // 数据高字节
    tx[2] = (uint8_t)(dacCode & 0xFF);            // 数据低字节
    DAC81408_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi3, tx, rx, 3, HAL_MAX_DELAY);
    DAC81408_CS_HIGH();  // CS上升沿 → 异步模式下DAC输出立即更新
		
    // 注意:电压范围已在 APPL_StartOutputHandler() 初始化时统一配置,
    // 运行时不再改写,避免与其他通道的范围寄存器相互覆盖
}

改动要点:

  • 所有 SPI 传输改为 3 字节(24-bit 完整帧)
  • 写 DAC 数据:0x14 + ch 即对应通道寄存器地址
e.应用主函数(周期性执行)
  • 这个函数是应用层的核心,会被协议栈周期性调用(默认 1ms 一次)
  • 在这里实现实际的硬件读写操作

对于这个 DAC 输出从站,APPL_Application() 可以保持很简单——因为 DAC 输出已经在 APPL_OutputMapping() 中完成了,PDO 事件会自动触发它。

/////////////////////////////////////////////////////////////////////////////////////////
/**
\brief    This function will called from the synchronisation ISR 
            or from the mainloop if no synchronisation is supported
*////////////////////////////////////////////////////////////////////////////////////////
void APPL_Application(void)
{
		// DAC输出已在APPL_OutputMapping()中由PDO事件触发写入
    // 此处可留空,或添加以下可选任务
}
f.添加 TxPDO 参数对象处理函数(必须添加)

上个基础工程添加了,本工程暂不添加。

g.APPL_StartOutputHandler() — 初始化 DAC

DAC81408 上电后默认处于掉电模式,内部基准也是关闭的。如果不初始化,DAC 根本不会输出。

/////////////////////////////////////////////////////////////////////////////////////////
/**
 \return    AL Status Code (see ecatslv.h ALSTATUSCODE_....)

 \brief    The function is called in the state transition from SAFEOP to OP when
             all general settings were checked to start the output handler. This function
             informs the application about the state transition, the application can refuse
             the state transition when returning an AL Status error code.
           The return code NOERROR_INWORK can be used, if the application cannot confirm
           the state transition immediately, in that case the application need to be complete 
           the transition by calling ECAT_StateChange.
*////////////////////////////////////////////////////////////////////////////////////////

UINT16 APPL_StartOutputHandler(void)
{
		uint8_t tx[3], rx[3];
	
	
    // ---- 1. 使能内部基准 + 退出上电掉电模式 ----
    // GENCONFIG (0x04): Bit14=0(基准开启), Bit5=0(退出掉电), 其他默认
    // 复位值 = 0x7F00 → Bit14=1(基准掉电), Bit5=1(设备掉电)
    // 所以需要写入 Bit14=0, Bit5=0 → 值 = 0x7F00 & ~(1<<14) & ~(1<<5) = 0x3F00
    tx[0] = 0x00 | DAC_REG_GENCONFIG;  // 0x04
    tx[1] = 0x3F;                        // 高字节
    tx[2] = 0x00;                        // 低字节
    DAC81408_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi3, tx, rx, 3, HAL_MAX_DELAY);
    DAC81408_CS_HIGH();
	
	
    // ---- 2. 设置所有通道电压范围为 ±10V (0x0A) ----
    // DACRANGE_H (0x0B): 通道7[15:12], 通道6[11:8], 通道5[7:4], 通道4[3:0]
    // DACRANGE_L (0x0C): 通道3[15:12], 通道2[11:8], 通道1[7:4],  通道0[3:0]
    // ±10V → 编码 0x0A (0000 1010)
    uint32_t rangeH = 0;
    uint32_t rangeL = 0;
    for (uint8_t ch = 0; ch < 4; ch++) {
        rangeH |= ((uint32_t)0x0A << (ch * 4));  // 通道4~7 → rangeH
        rangeL |= ((uint32_t)0x0A << (ch * 4));  // 通道0~3 → rangeL
    }
    // 写 DACRANGE_H (0x0B)
    tx[0] = 0x00 | DAC_REG_DACRANGE_H;
    tx[1] = (uint8_t)(rangeH >> 8);
    tx[2] = (uint8_t)(rangeH & 0xFF);
    DAC81408_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi3, tx, rx, 3, HAL_MAX_DELAY);
    DAC81408_CS_HIGH();
    // 写 DACRANGE_L (0x0C)
    tx[0] = 0x00 | DAC_REG_DACRANGE_L;
    tx[1] = (uint8_t)(rangeL >> 8);
    tx[2] = (uint8_t)(rangeL & 0xFF);
    DAC81408_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi3, tx, rx, 3, HAL_MAX_DELAY);
    DAC81408_CS_HIGH();
		
		
    // ---- 3. 清除所有通道上电复位状态(输出0V)----
    for (uint8_t ch = 0; ch < 8; ch++)
    {
        tx[0] = 0x00 | (DAC_REG_DAC0 + ch);
        tx[1] = 0x00;
        tx[2] = 0x00;
        DAC81408_CS_LOW();
        HAL_SPI_TransmitReceive(&hspi3, tx, rx, 3, HAL_MAX_DELAY);
        DAC81408_CS_HIGH();
    }
	
	
    return ALSTATUSCODE_NOERROR;
}
h.主函数部分(使用 mydacapp.c 的主函数)

#if USE_DEFAULT_MAIN
/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    This is the main function
*////////////////////////////////////////////////////////////////////////////////////////
int main(void)
{
 
 
    // ====================== EtherCAT协议栈初始化 ======================
    HW_Init();
    MainInit();
 
    // ====================== 主循环 ======================
    bRunApplication = TRUE;
    do
    {
        MainLoop();  // EtherCAT主任务
        
    } while (bRunApplication == TRUE);
 
    HW_Release();
    
    return 0;
}
#endif //#if USE_DEFAULT_MAIN

五.编译报错修改

1.解决 4 个中断函数重复定义(最优先)

打开stm32h7xx_it.c文件,找到并删除以下 4 个函数

删后:

/******************************************************************************/
/* STM32H7xx Peripheral Interrupt Handlers                                    */
/* Add here the Interrupt Handlers for the used peripherals.                  */
/* For the available peripheral interrupt handler names,                      */
/* please refer to the startup file (startup_stm32h7xx.s).                    */
/******************************************************************************/

/**
  * @brief This function handles EXTI line0 interrupt.
  */

/**
  * @brief This function handles EXTI line1 interrupt.
  */


/**
  * @brief This function handles EXTI line[9:5] interrupts.
  */


/**
  * @brief This function handles TIM6 global interrupt, DAC1_CH1 and DAC1_CH2 underrun error interrupts.
  */


/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

2.解决 main 函数重复定义

错误原因

  • mydacapp.c中使用了USE_DEFAULT_MAIN宏,所以mydacapp.c里有一个 main 函数

  • CubeMX 生成的main.c里也有一个 main 函数

  • 两个 main 函数导致链接错误

仍然使用方案 2:删除 main.c 里的 main 函数(√)

  • 打开main.c,删除int main(void)开始到函数最后一行的所有内容

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */
/* USER CODE BEGIN Boot_Mode_Sequence_0 */
#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
  int32_t timeout;
#endif /* DUAL_CORE_BOOT_SYNC_SEQUENCE */
/* USER CODE END Boot_Mode_Sequence_0 */

  /* MPU Configuration--------------------------------------------------------*/
  MPU_Config();

/* USER CODE BEGIN Boot_Mode_Sequence_1 */
#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
  /* Wait until CPU2 boots and enters in stop mode or timeout*/
  timeout = 0xFFFF;
  while((__HAL_RCC_GET_FLAG(RCC_FLAG_D2CKRDY) != RESET) && (timeout-- > 0));
  if ( timeout < 0 )
  {
  Error_Handler();
  }
#endif /* DUAL_CORE_BOOT_SYNC_SEQUENCE */
/* USER CODE END Boot_Mode_Sequence_1 */
  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();
/* USER CODE BEGIN Boot_Mode_Sequence_2 */
#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
/* When system initialization is finished, Cortex-M7 will release Cortex-M4 by means of
HSEM notification */
/*HW semaphore Clock enable*/
__HAL_RCC_HSEM_CLK_ENABLE();
/*Take HSEM */
HAL_HSEM_FastTake(HSEM_ID_0);
/*Release HSEM in order to notify the CPU2(CM4)*/
HAL_HSEM_Release(HSEM_ID_0,0);
/* wait until CPU2 wakes up from stop mode */
timeout = 0xFFFF;
while((__HAL_RCC_GET_FLAG(RCC_FLAG_D2CKRDY) == RESET) && (timeout-- > 0));
if ( timeout < 0 )
{
Error_Handler();
}
#endif /* DUAL_CORE_BOOT_SYNC_SEQUENCE */
/* USER CODE END Boot_Mode_Sequence_2 */

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SPI1_Init();
  MX_SPI3_Init();
  MX_TIM6_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

在 el9800hw.c 的 HW_Init() 中,删除了所有PIC单片机专属初始化代码,但是没有加入STM32的初始化代码

并且,我们用mydacapp.c中的main做主函数,删除了main.c中的Main函数,这里就是一个问题,main.c中的Main中初始化了STM32外设(如下代码段),但我全删了,需要在el9800hw.c中HW_Init() 中补充。

此外,ET1100从EEPROM存在启动未完成就进行SPI通信的风险:

        SPI发出,ET1100尚未就绪。

        // ET1100上电后需要从EEPROM加载配置,约30~100ms。

        // 如果此时SPI已发出,ET1100尚未就绪,do-while测试会永远失败

修改:1.在myapp.c的main函数中加入HAL_Init(),以保障延时函数的运行

2.el9800hw.c 的HW_Init(void)中加一行进行延时--

#if USE_DEFAULT_MAIN
/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    This is the main function
*////////////////////////////////////////////////////////////////////////////////////////
int main(void)
{
 
		/* 必须在 HW_Init() 之前调用!否则 HAL_Delay() 会卡死 */
    HAL_Init();	
	
    // ====================== EtherCAT协议栈初始化 ======================
    HW_Init();
    MainInit();
		
		/* MPU Configuration */
		MPU_Config();
    // ====================== 主循环 ======================
    bRunApplication = TRUE;
    do
    {
        MainLoop();  // EtherCAT主任务
        
    } while (bRunApplication == TRUE);
 
    HW_Release();
    
    return 0;
}
#endif //#if USE_DEFAULT_MAIN
UINT8 HW_Init(void)
{
    UINT32 intMask;
	
		// ====================== 删除所有PIC单片机专属初始化代码 ======================
    // PORT_CFG;          // PIC端口配置寄存器操作,STM32无效
    // SPI1_CON1 = ...;   // PIC SPI控制寄存器操作,STM32无效
    // SPI1_STAT = ...;   // PIC SPI状态寄存器操作,STM32无效
    // (_INT1IE)=1;       // PIC中断使能寄存器操作,STM32无效
	
		 /* Initialize all configured peripherals */
		MX_GPIO_Init();
		MX_SPI1_Init();
		MX_SPI3_Init();
		MX_TIM6_Init();
	
		
		 /* ====================== 2. 等待ET1100从EEPROM启动完成 ====================== */
    // ET1100上电后需要从EEPROM加载配置,约30~100ms。
    // 如果此时SPI已发出,ET1100尚未就绪,do-while测试会永远失败
    HAL_Delay(100);     // ← 必须有 HAL_Init() 先运行,否则 HAL_Delay() 会卡死
		
		// ====================== 官方保留:ESC通信可靠性测试(必须保留!) ======================
    // 向AL Event Mask寄存器(0x204)写入0x93,然后读回验证
    // 这是判断SPI通信是否正常、ET1100是否正常工作的唯一标准
    do
    {
        intMask = 0x93;
        HW_EscWriteDWord(intMask, ESC_AL_EVENTMASK_OFFSET);
        intMask = 0;
        HW_EscReadDWord(intMask, ESC_AL_EVENTMASK_OFFSET);
    } while (intMask != 0x93);
 
    // 测试通过,关闭所有中断掩码,等待协议栈开启需要的中断
    intMask = 0x00;
    HW_EscWriteDWord(intMask, ESC_AL_EVENTMASK_OFFSET);
 
    // ====================== 初始化中断和定时器(保留官方结构) ======================
    INIT_ESC_INT;       // 空宏,EXTI已在CubeMX中初始化
 
    INIT_SYNC0_INT;     // 空宏,EXTI已在CubeMX中初始化
    INIT_SYNC1_INT;     // 空宏,EXTI已在CubeMX中初始化
 
    ENABLE_SYNC0_INT;   // 使能SYNC0中断
    ENABLE_SYNC1_INT;   // 使能SYNC1中断
 
    INIT_ECAT_TIMER;    // 空宏,TIM6已在CubeMX中初始化
    START_ECAT_TIMER;   // 启动TIM6定时器中断
 
    // 使能全局中断
    ENABLE_GLOBAL_INT;
 
    return 0;
}


 

此后编译无报错但是存在警告

3.解决警告

A.

..\Src\sdoserv.h(647): warning:  #1295-D: Deprecated declaration SODS_ClearPendingResponse - give arg types
ROTO    void  SODS_ClearPendingResponse();
  • 这是SSC5.12 官方代码的 bug,不是我们的问题
  • 完全不影响程序运行,可以忽略
  • 如果想去掉警告,打开sdoserv.h第 647 行,改为:
PROTO    void  SODS_ClearPendingResponse(void);//括号中加入void

B.

../CM7/Core/Src/main.c(145): warning:  #177-D: function "MPU_Config"  was declared but never referenced
  void MPU_Config(void)
../CM7/Core/Src/main.c: 1 warning, 0 errors

CubeMX 在双核 STM32H755 项目中通常会生成 MPU_Config() 用于配置内存保护单元(MPU),但在某些模板代码中该函数没有自动调用。

修改:把 MPU_Config() 函数复制到 mydacapp.c 顶部(main() 之前),在 main() 中调用它

static void MPU_Config(void);
/* MPU Configuration */

void MPU_Config(void)
{
  MPU_Region_InitTypeDef MPU_InitStruct = {0};

  /* Disables the MPU */
  HAL_MPU_Disable();

  /** Initializes and configures the Region and the memory to be protected
  */
  MPU_InitStruct.Enable = MPU_REGION_ENABLE;
  MPU_InitStruct.Number = MPU_REGION_NUMBER0;
  MPU_InitStruct.BaseAddress = 0x0;
  MPU_InitStruct.Size = MPU_REGION_SIZE_4GB;
  MPU_InitStruct.SubRegionDisable = 0x87;
  MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
  MPU_InitStruct.AccessPermission = MPU_REGION_NO_ACCESS;
  MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
  MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
  MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
  MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

  HAL_MPU_ConfigRegion(&MPU_InitStruct);
  /* Enables the MPU */
  HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

}

#if USE_DEFAULT_MAIN
/////////////////////////////////////////////////////////////////////////////////////////
/**
 \brief    This is the main function
*////////////////////////////////////////////////////////////////////////////////////////
int main(void)
{
 
 
    // ====================== EtherCAT协议栈初始化 ======================
    HW_Init();
    MainInit();
		
		/* MPU Configuration */
		MPU_Config();
    // ====================== 主循环 ======================
    bRunApplication = TRUE;
    do
    {
        MainLoop();  // EtherCAT主任务
        
    } while (bRunApplication == TRUE);
 
    HW_Release();
    
    return 0;
}
#endif //#if USE_DEFAULT_MAIN

C.

compiling mydacapp.c...
..\Src\mydacapp.c(359): warning:  #1215-D: #warning directive: "Implement input (Slave -> Master) mapping"
      #warning "Implement input (Slave -> Master) mapping"
..\Src\mydacapp.c: 1 warning, 0 errors

注释掉这段就好

/////////////////////////////////////////////////////////////////////////////////////////
/**
\param      pData  pointer to input process data

\brief      This function will copies the inputs from the local memory to the ESC memory
            to the hardware
*////////////////////////////////////////////////////////////////////////////////////////
void APPL_InputMapping(UINT16* pData)
{
	/*
#if _WIN32
   #pragma message ("Warning: Implement input (Slave -> Master) mapping")
#else
    #warning "Implement input (Slave -> Master) mapping"
#endif
	*/
}

D.

..\Src\objdef.c(917): warning:  #767-D: conversion from pointer to smaller integer
              if((((
EM_ADDR)pVarPtr) & 0x1) == 0x1)
..\Src\objdef.c(924): warning:  #767-D: conversion from pointer to smaller integer
                  pVarPtr = (
INT16 
BXMEM *)(((
EM_ADDR)pVarPtr)& ~(MEM_ADDR)0x1);
..\Src\objdef.c: 2 warnings, 0 errors

这是 SSC 5.12 框架中的一个已知的编译器警告,不是代码问题。

MEM_ADDR 被定义为 UINT16(16 bit),但 STM32H7 的指针是 32 位的。把 32 位指针强制转成 16 位再与 0x1 做位运算,编译器认为高 16 位数据会丢失。

忽略它

4.下载程序识别不到

备份0528A

A.

双核配置问题会导致 JLINK 扫不到芯片。核心原因是 DUAL_CORE_BOOT_SYNC_SEQUENCE 同步握手可能挂死了。

main.c 第 42 行定义了这个宏:

#define DUAL_CORE_BOOT_SYNC_SEQUENCE

这个同步机制依赖 HSEM(硬件信号量):

#if defined(DUAL_CORE_BOOT_SYNC_SEQUENCE)
    // CM7 等待 CM4 释放 HSEM_ID_0
    while (HAL_HSEM_FastTake(HSEM_ID_0) != HAL_OK);
    // ...
    HAL_HSEM_Release(HSEM_ID_0, 0);
#endif

问题在于:CM4 的 Flash 区域可能是空的或者代码有问题,导致 CM4 根本没有释放 HSEM,CM7 就一直卡在 SystemInit() 里无法完成启动,芯片处于"半死不活"状态,JLINK 自然扫不到。

尝试:注释掉同步宏

// #define DUAL_CORE_BOOT_SYNC_SEQUENCE

释掉它后重新编译下载,CM7 就不再等待 CM4 的 HSEM 握手,应该能正常启动和调试。

没用

B.

项目文件列表把 CM4 的启动文件和 CM4 的源码也编进来了。

这会带来两个典型后果:

  1. 链接/向量表/入口可能冲突(两个 startup、两个 main.c
  2. Keil 的 Device 选择很容易被搞成 :CM4,从而调试器按 CM4 去连,出现各种异常

从 DACslave0527A_CM7 里移除/排除 CM4 文件

在 Project 树里,右键这些文件,选择 Options for File(或 Manage Project Items):

  • startup_stm32h755xx_CM4.s
  • Application/User/CM4/Core 下面的 main.c / stm32h7xx_it.c / stm32h7xx_hal_msp.c

六.双核供电模式选择

纯 LDO 模式,也就是:Supply Config 1: LDO only (SMPS OFF, LDO ON)

  • 内部 SMPS 不工作
  • 芯片内部用 LDO 从输入电源生成 VCORE
  • VCAP 上挂电容即可

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐