TWINCAT主站端实现对从站PG8-PG15 → LED 输出的控制,以及读取从站的PC8-PC15 → 拨码开关输入
SSC生成协议栈,除PDO\SDO部分其余均与第一篇文章一致,还有16位要改
为什么 32 位的 STM32F407 却选 16 位?
STM32F407 RAM 缓冲区》SPI1(逐字节传输)》ET1100 ESC DPRAM
通过 SPI 访问 ET1100 时,数据是逐字节串行发送的,STM32 的 32 位总线宽度在这里完全用不上。ET1100 的寄存器和数据区本质上是字节/16位对齐的(EtherCAT 协议本身基于16位对齐设计)。
更重要的历史原因是:_STM32_IO8 = 1这个分支本来是为 PIC24HJ(16位 MCU)写的,SSC 工具将其复用到 STM32 上时,直接继承了 PIC24 的内存组织方式,即 16 位缓冲区布局,而没有必要改成 32 位——因为:
- SPI 传输本身是按字节进行的,缓冲区是
UINT16[]还是UINT32[]对 SPI 传输没有任何影响 - STM32F407 的 Cortex-M4 访问
UINT16[]和UINT32[]都同样高效,不存在性能损失 - 改成
CONTROLLER_32BIT = 1反而会让GET_MEM_SIZE变为 4 字节对齐,造成内存浪费(每个字节数据向上对齐到 4 字节)
PDO/SDO
将MAX_MBX_SIZE从 0x0080 改为 0x0100。
原因:MAX_MBX_SIZE是主站可以协商的邮箱上限。参考工程设为 0x0100(256字节),允许主站在需要时使用更大的邮箱缓冲区(比如读取较长的 SDO Info 对象名称字符串)。你当前设为 0x0080 与 DEF 相同,意味着邮箱大小被锁死在 128 字节,虽然基本功能能跑,但在 TwinCAT 执行 SDO Info 扫描时可能出现截断。
保存好后,通过 Tool → Application → Create New 弹出 Excel 表格,新生成的表格如下图:
该表格是用来配置对象字典,通过SSC工具导入该表格将自动生成EtherCAT从站代码和XML设备描述文件(ESI)
Excel 对象字典表(Object Dictionary Spreadsheet)。这张表是整个从站开发最核心的配置文件,SSC 工具会根据它自动生成el9800appl.c/h、PDO 映射代码等。下面详细说明如何填写。
| 列名 | 说明 |
|---|---|
Index | 对象索引,如 |
ObjectCode | 对象类型: |
SI | SubIndex 编号(0=子索引数量,1起为实际条目) |
DataType | 数据类型: |
Name | TwinCAT 中显示的名称 |
Default | 默认值 |
R/S |
|
Access | 留空即可(工具自动处理) |
rx/tx |
|
CoeRead/CoeWrite | 自定义读写回调,一般留空 |
重要提示(截图中的 Usage Notes 说明):
- PDO 映射对象(
0x1601、0x1A00)和 SyncManager 对象(0x1C12、0x1C13)不需要填写,SSC 工具会自动生成 0x1000、0x1001、0x1008等标准对象不需要填写,同样自动生成- 8bit 以下的条目不能跨字节边界(每个对象总位数需为 8 的倍数,用
BIT8对齐)
现在我们需要在//0x6nnx(第35行区域)下面填入 0x6000 拨码开关输入,在//0x7nnx(第38行区域)下面填入 0x7010 LED 输出。
对象 0x6000:拨码开关输入(TxPDO,从站→主站)
SubIndex 9 的BIT8是对齐填充:8个 BOOL(各1bit)合计8bit,加上 SI0 是 UINT8(8bit),共16bit = 2字节,刚好对齐。
SSC 工具自动为每个 RECORD 对象添加 SubIndex0,你在 Excel 里再写一行 SI=0 就重复了。
修改方法:删除 0x6000 和 0x7010 中的 SubIndex0 行:
对象 0x7010:LED 输出(RxPDO,主站→从站)
表格填写完成后,回到 SSC Tool 主界面:
File → Save保存 Excel 表格,改名myapp- 切换到 SSC Tool 窗口,点击
Build → Create New Slave Files
工具会自动生成:
el9800appl.h:包含TOBJ6000(开关结构体)和TOBJ7010(LED结构体)el9800appl.c:包含对象字典数组ApplicationObjDic[]和 PDO 映射初始值- 同时自动生成
0x1A00(TxPDO映射,映射到0x6000)、0x1601(RxPDO映射,映射到0x7010)
导入有报错,SSC中重复定义了SubIndex0,删掉重新导入
| 生成文件 | 说明 |
|---|---|
| 应用主文件,含 |
| PDO 数据结构定义、对象字典(由 Excel 自动生成) |
| 硬件抽象层(名字不变,固定为此名) |
| 硬件宏接口(名字不变) |
| 协议栈核心(不变) |
| 邮箱(不变) |
| CoE(不变) |
| 应用调度(不变) |
| 配置宏(不变) |
参考工程的el9800appl.c/h替换为你生成的myapp.c/h
在Myappl.c中找到APPL_Application()函数,添加 IO 读写逻辑:
从myappObjects.h中可以看到,SSC 为你的对象生成了以下变量名:
| 对象 | 变量名 | 成员名 |
|---|---|---|
0x6000 DI Inputs |
|
|
0x7010 DO Outputs |
|
|
需要修改myapp.c的三个函数
原来三个函数体内的内容是编译警告占位符,不是实际逻辑,必须删掉再填入你的代码:
函数一:APPL_OutputMapping()— 接收主站下发的 LED 控制数据
void APPL_OutputMapping(UINT16* pData) { /* pData 指向 RxPDO 数据区(主站→从站) * 低字节 bit0~bit7 依次对应 LED1~LED8 */ UINT8 led_byte = (UINT8)(*pData & 0x00FF); DOOutputs0x7010.LED1 = (led_byte >> 0) & 0x01; DOOutputs0x7010.LED2 = (led_byte >> 1) & 0x01; DOOutputs0x7010.LED3 = (led_byte >> 2) & 0x01; DOOutputs0x7010.LED4 = (led_byte >> 3) & 0x01; DOOutputs0x7010.LED5 = (led_byte >> 4) & 0x01; DOOutputs0x7010.LED6 = (led_byte >> 5) & 0x01; DOOutputs0x7010.LED7 = (led_byte >> 6) & 0x01; DOOutputs0x7010.LED8 = (led_byte >> 7) & 0x01; }函数二:APPL_Application()— 读 GPIO 开关 + 驱动 LED
void APPL_Application(void) { /* ---- 读拨码开关 PC8~PC15 → 更新 DIInputs0x6000 ---- */ /* PC 上拉输入:拨码ON接地=低电平,逻辑取反得1 */ DIInputs0x6000.Switch1 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_8) ? 0 : 1; DIInputs0x6000.Switch2 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_9) ? 0 : 1; DIInputs0x6000.Switch3 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_10) ? 0 : 1; DIInputs0x6000.Switch4 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_11) ? 0 : 1; DIInputs0x6000.Switch5 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_12) ? 0 : 1; DIInputs0x6000.Switch6 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) ? 0 : 1; DIInputs0x6000.Switch7 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) ? 0 : 1; DIInputs0x6000.Switch8 = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_15) ? 0 : 1; /* ---- 从 DOOutputs0x7010 驱动 LED PG8~PG15 ---- */ GPIO_WriteBit(GPIOG, GPIO_Pin_8, DOOutputs0x7010.LED1 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_9, DOOutputs0x7010.LED2 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_10, DOOutputs0x7010.LED3 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_11, DOOutputs0x7010.LED4 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_12, DOOutputs0x7010.LED5 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_13, DOOutputs0x7010.LED6 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_14, DOOutputs0x7010.LED7 ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOG, GPIO_Pin_15, DOOutputs0x7010.LED8 ? Bit_SET : Bit_RESET); }函数三:APPL_InputMapping()— 打包开关状态发给主站
void APPL_InputMapping(UINT16* pData) { /* 将 DIInputs0x6000 各 bit 打包进 TxPDO 数据区(从站→主站) * 低字节 bit0~bit7 依次对应 Switch1~Switch8,高字节为 Align=0 */ UINT8 sw_byte = 0; sw_byte |= (DIInputs0x6000.Switch1 & 0x01) << 0; sw_byte |= (DIInputs0x6000.Switch2 & 0x01) << 1; sw_byte |= (DIInputs0x6000.Switch3 & 0x01) << 2; sw_byte |= (DIInputs0x6000.Switch4 & 0x01) << 3; sw_byte |= (DIInputs0x6000.Switch5 & 0x01) << 4; sw_byte |= (DIInputs0x6000.Switch6 & 0x01) << 5; sw_byte |= (DIInputs0x6000.Switch7 & 0x01) << 6; sw_byte |= (DIInputs0x6000.Switch8 & 0x01) << 7; *pData = (UINT16)sw_byte; /* 高字节 Align 自动为 0 */ }补充:APPL_StopOutputHandler()— 离开 OP 时关闭所有 LED
UINT16 APPL_StopOutputHandler(void) { GPIO_ResetBits(GPIOG, GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15); return ALSTATUSCODE_NOERROR; }一处额外修改:main()返回类型
注意myapp.c底部的main()用的是_PIC24判断,而不是_STM32_IO8,在 Keil 中会警告:
#if USE_DEFAULT_MAIN #if _PIC24 int main(void) #else void main(void) // ← Keil 会警告 #endif将这段改为:
#if USE_DEFAULT_MAIN #if _PIC24 || _STM32_IO8 int main(void) #else void main(void) #endif有问题main()有int返回值但缺少return 0;
myapp.c第 366 行你已改为:
#if _PIC24 || _STM32_IO8 int main(void)但是 SSC 5.12 的ecat_def.h里没有_STM32_IO8这个宏(SSC 5.12 已删除此宏)。而结尾处return 0;仍在#if _PIC24下,导致:若你在 Keil 全局定义了_STM32_IO8,函数签名是int main(void)但函数体没有return 0;,Keil 会报警告/错误。
修复方法:直接把main()这段改为:
#if USE_DEFAULT_MAIN ///////////////////////////////////////////////////////////////////////////////////////// /** \brief This is the main function *//////////////////////////////////////////////////////////////////////////////////////// int main(void) { HW_Init(); MainInit(); bRunApplication = TRUE; do { MainLoop(); } while (bRunApplication == TRUE); HW_Release(); return 0; } #endif //#if USE_DEFAULT_MAIN /** @} */数据流全貌
主站写 RxPDO(LED控制字节) ↓ APPL_OutputMapping() ── 解包bit → DOOutputs0x7010.LED1~LED8 ↓ APPL_Application() ── DOOutputs0x7010 → 驱动 PG8~PG15 ── 读 PC8~PC15 → DIInputs0x6000.Switch1~8 ↓ APPL_InputMapping() ── 打包bit ← DIInputs0x6000.Switch1~8 ↓ 主站读 TxPDO(开关状态字节)问题 2:el9800hw.h缺少所有 STM32 硬件宏——这是移植的核心工作(先去创建32工程再返回这一步)
查看你生成的el9800hw.h,与参考工程相比,以下内容完全缺失,必须手动补充
/* 需要在 el9800hw.h 的 #include 部分添加 */ #include "stm32f4xx.h" #include "SPI1.h" /* 提供 SELECT_SPI / DESELECT_SPI / WR_CMD *//* 需要添加的硬件宏定义 */ /* 定时器 */ #define ECAT_TIMER_INC_P_MS 2000 #define HW_GetTimer() (TIM9->CNT) #define HW_ClearTimer() ((TIM9->CNT) = 0) /* AL Event 中断控制 */ #define DISABLE_ESC_INT() NVIC_DisableIRQ(EXTI3_IRQn) #define ENABLE_ESC_INT() NVIC_EnableIRQ(EXTI3_IRQn) /* 定时器中断控制 */ #define INIT_ECAT_TIMER TIM_Configuration(10) #define START_ECAT_TIMER TIM_Cmd(TIM9, ENABLE) #define STOP_ECAT_TIMER TIM_Cmd(TIM9, DISABLE) /* ESC 中断初始化 */ #define INIT_ESC_INT EXTI3_Configuration() /* DC Sync 中断初始化 */ #define INIT_SYNC0_INT EXTI1_Configuration() #define INIT_SYNC1_INT EXTI2_Configuration() /* 拨码开关输入 PC8~PC15 */ #define SWITCH_1 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_8) #define SWITCH_2 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_9) #define SWITCH_3 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_10) #define SWITCH_4 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_11) #define SWITCH_5 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_12) #define SWITCH_6 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) #define SWITCH_7 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) #define SWITCH_8 GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_15) /* LED 输出 PG8~PG15 */ #define LED_1 PGout(8) #define LED_2 PGout(9) #define LED_3 PGout(10) #define LED_4 PGout(11) #define LED_5 PGout(12) #define LED_6 PGout(13) #define LED_7 PGout(14) #define LED_8 PGout(15)这些宏添加在el9800hw.h的#ifndef _EL9800HW_H_和#define _EL9800HW_H_之后、现有宏定义区域内。
STM32基础工程的创建
首先是时钟,选好之后先配置时钟树
然后SPI配置
左侧Connectivity → SPI1:
Mode 页面:
| 参数 | 值 |
|---|---|
Mode | Full-Duplex Master |
Hardware NSS Signal | Disable(软件控制CS) |
Configuration → Parameter Settings 页面:
| 参数 | 值 | 说明 |
|---|---|---|
Frame Format | Motorola | |
Data Size | 8 Bits | |
First Bit | MSB First | |
Prescaler (for Baud Rate) | 8 | 84MHz/8 = 10.5 MHz |
Clock Polarity (CPOL) | High | SPI Mode 3 |
Clock Phase (CPHA) | 2 Edge | SPI Mode 3 |
CRC Calculation | Disabled | |
NSS Signal Type | Software |
引脚确认(Pinout 自动分配或手动点击引脚):
| 信号 | 引脚 |
|---|---|
SPI1_SCK | PB3 |
SPI1_MISO | PB4 |
SPI1_MOSI | PB5 |
GPIO 配置(逐一点击 Pinout 图中的引脚)
PE2 — ET1100 片选(SPI CS)
点击PE2→ 选GPIO_Output,然后在GPIO页配置:
| 参数 | 值 |
|---|---|
GPIO output level | High(默认未选中) |
GPIO mode | Output Push Pull |
GPIO Pull-up/Pull-down | No pull-up, no pull-down |
Speed | High |
User Label |
|
PC8 ~ PC15 — 拨码开关输入(8个,逐一配置)
点击每个引脚 →GPIO_Input:
| 参数 | 值 |
|---|---|
GPIO mode | Input mode |
GPIO Pull-up/Pull-down | Pull-up(拨码ON=接地=低电平) |
User Label |
|
PG8 ~ PG15 — LED 输出(8个,逐一配置)
点击每个引脚 →GPIO_Output:
| 参数 | 值 |
|---|---|
GPIO output level | Low(默认熄灭) |
GPIO mode | Output Push Pull |
GPIO Pull-up/Pull-down | No pull |
Speed | Low |
User Label |
|
EXTI 中断配置(3路)
PE3 — AL Event 中断(最重要)
点击PE3→ 选GPIO_EXTI3:
| 参数 | 值 |
|---|---|
GPIO mode | External Interrupt Mode with Falling edge trigger detection |
GPIO Pull-up/Pull-down | No pull-up, no pull-down |
User Label |
|
PC1 — DC Sync0 中断
点击PC1→ 选GPIO_EXTI1:
| 参数 | 值 |
|---|---|
GPIO mode | External Interrupt Mode with Falling edge trigger |
Pull | No pull |
User Label |
|
PC2 — DC Sync1 中断
点击PC2→ 选GPIO_EXTI2,同上。
TIM9 配置(EtherCAT 心跳定时器)
左侧Timers → TIM9:
Mode:
| 参数 | 值 |
|---|---|
Clock Source | Internal Clock |
Parameter Settings:
APB2 Timer Clock = 168MHz(APB2=84MHz,×2=168MHz)
| 参数 | 值 | 说明 |
|---|---|---|
Prescaler | 83 | 168MHz/(83+1)=2MHz |
Counter Mode | Up | |
Counter Period (ARR) | 1999 | 2MHz/2000=1kHz,即1ms中断 |
auto-reload preload | Enable | |
NVIC:TIM1_BRK_TIM9 | 开启 | Preemption Priority = 2 |
CubeMX 里 ARR 填 1999(对应 StdPeriph 的
TIM_Period=2000,两者含义相同,都是计数 0~1999 共 2000 个 tick)。
el9800hw.h定时器宏对应关系确认
#define ECAT_TIMER_INC_P_MS 2000 /* 2MHz计数,1ms=2000tick */ #define HW_GetTimer() (__HAL_TIM_GET_COUNTER(&htim9)) #define HW_ClearTimer() (__HAL_TIM_SET_COUNTER(&htim9, 0))这三个值与参考工程完全一致,不需要改。
NVIC 优先级配置
左侧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
然后各中断设置:
| 中断名称 | Preemption Priority | Sub Priority |
|---|---|---|
EXTI line3 interrupt | 0 | 0 |
EXTI line1 interrupt | 1 | 1 |
EXTI line2 interrupt | 1 | 1 |
TIM1 break interrupt and TIM9 global interrupt | 1(注意) | 1 |
注意:参考工程 TIM9 的抢占优先级写的是
2,但在NVIC_PriorityGroup_1下抢占优先级只有 0 和 1 两个值(1位),写2实际被截断为0,等效于抢占优先级 0,子优先级 1。这是参考工程的一个小问题。CubeMX 中建议直接填写:| TIM9 | Preemption: 1 | Sub: 1 |
与 Sync0/Sync1 相同,确保 AL Event(抢占0)仍是最高优先级,三者都低于它即可。
点击SYS→DEBUG选择serial wire
Project Manager 设置
接下来是移植(仅看个大概-具体操作参考下一篇文章)
操作清单(按顺序执行)
- 1. 将 SSC 文件复制到
Ethercat/目录并加入 Keil 工程 - 2. 添加
Ethercat/Inc到 include path - 3.
ecat_def.h中设置USE_DEFAULT_MAIN 0 - 4.
el9800hw.h添加上述 STM32 HAL 宏 - 5.
el9800hw.c将 SPI 收发替换为HAL_SPI_TransmitReceive - 6.
main.c加入HW_Init()/MainInit()/MainLoop() - 7.
stm32f4xx_it.c挂接 4 个中断函数 - 8. 编译,先排除
#warning类警告,再解决 error
第一步:目录结构规划
YourProject/ ├── Core/ │ ├── Src/ │ │ ├── main.c ← CubeMX 生成,需添加调用 │ │ └── stm32f4xx_it.c ← CubeMX 生成,需挂 SSC ISR │ └── Inc/ ├── Ethercat/ ← 新建,放所有 SSC 文件 │ ├── Src/ │ │ ├── myapp.c ← 你修改好的应用层 │ │ ├── ecatslv.c │ │ ├── mailbox.c │ │ ├── ecatcoe.c │ │ ├── ecatappl.c │ │ ├── objdef.c │ │ ├── sdoserv.c │ │ ├── coeappl.c │ │ └── el9800hw.c ← 需要改造为 HAL 版本 │ └── Inc/ │ ├── myapp.h │ ├── myappObjects.h │ ├── ecat_def.h │ ├── ecatslv.h │ ├── mailbox.h │ ├── ecatcoe.h │ ├── ecatappl.h │ ├── applInterface.h │ ├── objdef.h │ ├── sdoserv.h │ ├── coeappl.h │ ├── esc.h │ └── el9800hw.h ← 需要添加 STM32 HAL 宏 ├── Drivers/ ← CubeMX 生成的 HAL 库 └── MDK-ARM/ └── YourProject.uvprojx第二步:Keil 工程中添加文件和路径
在 Keil 中右键Project→Manage Project Items:
- 新建
EtherCAT_Stack组,添加Ethercat/Src/下所有.c文件
在Options for Target → C/C++ → Include Paths中追加:
../Ethercat/Inc
在Define中追加(CubeMX 已生成了 STM32 的那些,再加):
USE_HAL_DRIVER,STM32F407xx第三步:改造el9800hw.h——添加 STM32 HAL 宏
文件结构很清晰。需要做两处修改:
第一处:在 Includes 区域补一个 HAL 头文件(NVIC_DisableIRQ等函数需要它)
第二处:在两个空白的注释区域填入4个宏
具体操作,在第33行#include "esc.h"下方、第42行#define ESC_RD上方,插入一行 include:
#include "esc.h" #include "stm32f4xx_hal.h"然后在两个空白注释区域填入宏定义:
/*--------------------------------------------- - hardware timer settings -----------------------------------------------*/ /* TIM9: APB2=168MHz, Prescaler=83 → timer clock=2MHz, 1ms=2000 ticks */ #define ECAT_TIMER_INC_P_MS 2000 #ifndef HW_GetTimer #define HW_GetTimer() (TIM9->CNT) #endif #ifndef HW_ClearTimer #define HW_ClearTimer() (TIM9->CNT = 0) #endif /*--------------------------------------------- - Interrupt and Timer defines -----------------------------------------------*/ /* AL Event interrupt: PE3 → EXTI3 */ #ifndef DISABLE_ESC_INT #define DISABLE_ESC_INT() NVIC_DisableIRQ(EXTI3_IRQn) #endif #ifndef ENABLE_ESC_INT #define ENABLE_ESC_INT() NVIC_EnableIRQ(EXTI3_IRQn) #endifel9800hw.h修改完毕,内容完全正确。解释一下每处改动的原因:
第34行#include "stm32f4xx_hal.h"NVIC_DisableIRQ/NVIC_EnableIRQ是 CMSIS 函数,TIM9寄存器结构体也需要 HAL 头带入。不加这个,凡是包含el9800hw.h的 SSC 文件(如ecatslv.c)编译时都会找不到这些符号。
第69行ECAT_TIMER_INC_P_MS 2000ecatappl.c第152行有#warning "Define the timer ticks per ms"检查,ECAT_CheckTimer()内部的 DC 时间戳计算(每次调用加 1000000 ns = 1ms)依赖这个值正确。数值推导:APB2=168MHz,TIM9 Prescaler=83 → 时钟 = 168M/84 = 2MHz → 每毫秒计 2000 个 tick,与参考工程完全一致。
第72~76行HW_GetTimer/HW_ClearTimerecatappl.c中计算总线周期时间时使用这两个宏直接读写TIM9->CNT,用#ifndef保护,将来若需要覆盖也方便。
第83~87行DISABLE_ESC_INT/ENABLE_ESC_INTecatslv.c第255/280行、ecatappl.c第804/814行都用到。作用是在 SM 同步数据处理期间短暂关闭 AL 中断防止重入,对应 PE3(EXTI3)。
el9800hw.h完成后,接下来按上次分析的顺序,还需要操作:
stm32f4xx_it.c— 添加HAL_GPIO_EXTI_Callback和HAL_TIM_PeriodElapsedCallbackmain.c— 在初始化后调用HW_Init()/MainInit(),循环里调用MainLoop()
缺口二:stm32f4xx_it.c缺少 HAL 回调函数实现
/* ---- STM32 HAL 平台适配 ---- */ #include "stm32f4xx_hal.h" /* SPI + 片选(PE2) */ extern SPI_HandleTypeDef hspi1; #define SELECT_SPI HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_RESET) #define DESELECT_SPI HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_SET) #define SPI_SEND_BYTE(b) ({ \ uint8_t _r; \ HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)&(b), &_r, 1, 10); \ _r; }) /* 定时器(TIM9) */ extern TIM_HandleTypeDef htim9; #define ECAT_TIMER_INC_P_MS 2000 #define HW_GetTimer() __HAL_TIM_GET_COUNTER(&htim9) #define HW_ClearTimer() __HAL_TIM_SET_COUNTER(&htim9, 0) /* AL Event 中断(EXTI3,PE3) */ #define DISABLE_ESC_INT() HAL_NVIC_DisableIRQ(EXTI3_IRQn) #define ENABLE_ESC_INT() HAL_NVIC_EnableIRQ(EXTI3_IRQn) /* 初始化宏(在 HW_Init 中已调用 CubeMX 的 MX_ 函数,这里留空或做二次配置) */ #define INIT_ESC_INT /* 由 CubeMX 的 MX_GPIO_Init() 完成 */ #define INIT_ECAT_TIMER HAL_TIM_Base_Start_IT(&htim9) #define START_ECAT_TIMER HAL_TIM_Base_Start_IT(&htim9) #define STOP_ECAT_TIMER HAL_TIM_Base_Stop_IT(&htim9) /* DC Sync(EXTI1=PC1, EXTI2=PC2) */ #define INIT_SYNC0_INT /* 由 CubeMX 完成 */ #define INIT_SYNC1_INT /* 由 CubeMX 完成 */ #define DISABLE_SYNC0_INT() HAL_NVIC_DisableIRQ(EXTI1_IRQn) #define ENABLE_SYNC0_INT() HAL_NVIC_EnableIRQ(EXTI1_IRQn) #define DISABLE_SYNC1_INT() HAL_NVIC_DisableIRQ(EXTI2_IRQn) #define ENABLE_SYNC1_INT() HAL_NVIC_EnableIRQ(EXTI2_IRQn) /* 拨码开关输入:PC8~PC15 */ #define SWITCH_1 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_8) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_2 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_9) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_3 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_10) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_4 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_11) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_5 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_12) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_6 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_7 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_14) == GPIO_PIN_RESET ? 1 : 0) #define SWITCH_8 (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_15) == GPIO_PIN_RESET ? 1 : 0) /* LED 输出:PG8~PG15 */ #define LED_1 HAL_GPIO_WritePin(GPIOG, GPIO_PIN_8, (GPIO_PinState)(v)) /* 用法特殊,建议改为函数,见 el9800hw.c 中 LED 驱动方式 */LED 宏因为 HAL 写法需要传值,建议在myapp.c的APPL_Application()中直接用HAL_GPIO_WritePin(),不必依赖宏。
第四步:改造el9800hw.c中的 SPI 读写函数
找到HW_EscRead和HW_EscWrite,将底层字节收发替换为 HAL:
/* 原来的 PIC SPI 收发字节函数,替换为 HAL 版本 */ static UINT8 SPITransfer(UINT8 txByte) { UINT8 rxByte = 0; HAL_SPI_TransmitReceive(&hspi1, &txByte, &rxByte, 1, 10); return rxByte; }然后在HW_EscRead/HW_EscWrite中所有调用原来 PIC SPI 寄存器的地方(SPI1BUF、WriteSPI()等)全部替换为SPITransfer(byte)。