STM32与LAN9252构建EtherCAT从站(四):STM32配置SPI

  1. 一、 STM32与LAN9252构建EtherCAT从站(一):项目简介
  2. 二、 STM32与LAN9252构建EtherCAT从站(二):使用SSC生成EtherCAT协议栈和XML文件
  3. 三、 STM32与LAN9252构建EtherCAT从站(三):LAN9252的XML文件
  4. 四、 STM32与LAN9252构建EtherCAT从站(四):STM32配置SPI
  5. 五、 STM32与LAN9252构建EtherCAT从站(五):STM32与LAN9252适配
  6. 六、 STM32与LAN9252构建EtherCAT从站(六):TwinCAT2的使用和从站测试

STM32与LAN9252构建EtherCAT从站(四):STM32配置SPI

这一章主要讲解STM32的SPI配置,以及与LAN9252的SPI接口对接。
从2018年开始,几乎所有最新的STM32项目或者教程,都不约而同地开始使用HAL库,其原因主要是意法半导体推出的STM32CubeMx这款图形化配置软件太好用了。一定程度上弥补了HAL库与标准库在使用习惯,已有项目遗产上的差距。而且,官方声明最新的STM32F7系列将只支持HAL库。
在这里我建议大家以积极的态度拥抱变化,STM32 ARM所有家族产品尽量都使用HAL库进行开发,并使用CubeMX软件辅佐开发;对于寄存器模式开发,在项目中如果有所接触,可以去翻阅手册查看原理,有价值的一些快速寄存器操作也是有必要记下来的,在一些高速场合确实很适用,比如快速开关中断,快速GPIO操作等;对于标准库模式开发,建议放弃了,标准库很久不更新了,现在已经成老古董了,再过几年就彻底跟不上时代了,就好比没人愿意用Github上5年以上不更新的项目,即使它确实很优秀。有人说【HAL臃肿,F7性能好,而且只能用HAL,所以用HAL,F0,F1,F3还是继续用标准库合适】,这种想法确实没必要,对于所有系列,还是统一学习一种库方便,而且就我目前接触过的项目,还没有什么是HAL搞不定,标准库能搞定的。

使用STM32CubeMX软件配置MCU外设

我并不打算将本系列文章写成一个关于STM32,尤其是关于STM32、Keil、CubeMX大而全的基础教程,那种每个界面都有截图,每个按钮的点击都要介绍,篇幅拉得太长了。我这里关于这些知识只能用简要地说明一下,具体到每个软件或者技术的详细使用,列为感兴趣,或者追求细节的话,还是建议去搜一些颗粒度更小,更专业的教程。
打开STM32CubeMX,新建一个STM32F103ZExx工程,我们来配置一下与LAN9252通信的外设。

与LAN9252通信,涉及到的外设包括以下几点:
  1. SPI:4线标准SPI,LAN9252支持SQI(6线高速SPI),但我们这里一切从简。标准模式下,SPI时钟频率最高支持30M。LAN9252还有高速SPI模式,时钟频率最高80M,这个自己去研究。

  2. 1ms的定时器:命名为ECAT_CheckTimer,协议栈的日常处理,包括看门狗喂狗行为,在定时器中断服务函数中进行,一般配置成1ms。

  3. 3个外部中断:包括1个EtherCAT主中断,命名为PDI_Isr;2个时钟同步IRQ,命名为Sync0_Isr,Sync1_Isr,EtherCAT主从通信中如果选择使用分布式时钟功能,这两个中断要配上。

NVIC方面,使用CubeMX配置时注意以下几点:
  1. 对于MCU来说,SPI是主站,不需要中断读写,SPI的通信时序是读的同时写,写的同时读,读写不分离,所以没必要配置中断。

  2. 3个外部中断,在CubeMX中都配置成初始enable,这样生成的代码友好一点,但到了Keil中,需要把初始化函数中最后一行Enable的语句给注释掉。什么时候Enable中断,什么时候Disable中断,都要由协议栈决定,而不是一上电就打开外部中断,那样非常容易出错,基本连不上。

  3. 定时器中断,跟外部中断一样,CubeMX中配置好enable,代码生成后注释掉最后一行Enable,由协议栈来决定打开和关闭定时器中断。

  4. 向量表的详细介绍可以参考我的另一篇博客:STM32F4+DP83848以太网通信指南系列(三):中断向量。一个可行的中断向量表为2bit抢先,2bit响应:
    a). ECAT_CheckTimer 配置为 1抢先,0响应
    b). PDI_Isr 配置为 1抢先,0响应
    c). Sync0_Isr,Sync1_Isr 均配置成 2抢先,0响应

CubeMX配置时的一些重要截图如下:
时钟:

Clock

SPI:

SPI

外部中断:

extern

NVIC:

nvic

KEIL5 工程模板:

keil5

模块化代码:

model
以上就是使用CubeMX配置的跟LAN9252通信的所有外设资源。点击右上方的【GENERATE CODE】按钮,自动生成Keil5 工程代码。下面的工作就跟CubeMX无关了,可以关掉了。

Keil5中适配SPI

CubeMX帮我们生成了Keil工程,我们的整个项目的后续开发也将以此套工程代码为蓝本继续下去。首先熟悉一下工程文件树。
tree
这是CubeMX帮我们生成的工程架构,其他都是浮云,中间的Application/User是主战场,我们自己的逻辑一般都在这个Group中,毫无疑问,main.c是主程序;gpio.c,spi.c,tim.c中有各个外设资源初始化的代码;stm32f1xx_it.c中有帮我们生成好的中断服务函数,里面有一些关键函数我们待会儿需要移动到main.c中;stm32f1xx_hal_msp.c中是外设资源与硬件资源的映射配置代码,如果有些外设,比如SPI,使用的是其他GPIO复用,将在这个文件中有所体现。此时点击Keil5中的编译按钮,应该能够顺利编译,无任何报警和错误的。

所谓的EtherCAT协议栈移植,其实就是将上面讲的各个外设资源的接口与EtherCAT协议栈的内部接口进行对接,比如SPI部分,我们需要封装一些简单的SPI操作,命名为协议栈期望的函数名,供协议栈调用;再比如3个外部中断和1个时间中断,我们需要在这4个中断服务函数中调用协议栈暴露给我们的4个不同的函数来适配协议栈。我们这个教程的本章节,将主要介绍SPI的适配,其他适配将留到后面章节介绍。

LAN9252 芯片的SSC模板

之前我们在STM32与LAN9252构建EtherCAT从站(二):使用SSC生成EtherCAT协议栈和XML文件中,使用SSC生成的EtherCAT从站代码是基于EL9800学习板的,那个学习板上的PHY芯片是ET1100这颗倍福自己的PHY芯片。我们这里使用相对廉价的LAN9252作为从站PHY芯片,LAN9252的一些寄存器设置跟ET1100是有区别的,因此我们到microchip官网下载LAN9252的SSC SDK。

下载回来以后,配合SSC5.11版本(5.12版本太新,跟LAN9252官方SDK兼容性不好),参考microchip的官方文档AN1916,生成的代码如下:
SRC
跟之前SSC生成的标准协议栈有所区别的是图片中第1,第2个文件由el9800.c,el9800.h变成了9525_HW.c,9252_HW.h;另外在LAN9252的SSC SDK中有一个SPI的工程文件,里面有SPI驱动的两个文件SPIDriver.c,SPIDriver.h,如果生成协议栈时没有add进去,这里手动复制一下;此外最后三个文件,STM32_EtherCAT_Slave开头的,是根据SSC中配置IO时的Excel文件生成的,其中STM32_EtherCAT_SlaveObjects.h是最有价值的,跟XML文件是一一对应的。
将这些文件全都copy到STM32工程的一个目录中,比如新建一个EtherCAT目录。
在KEIL的工程结构中新建2个Group,比如命名为EtherCAT,和Porting,按照图示,引入以下文件:
EtherCAT
至此,当前所看到的全部文件,均原封不动地来自于CubeMX,SSC和LAN9252 SDK,我们暂未对此进行任何修改。此时编译将引发大量错误,不过不要担心,根据Keil提示一定可以慢慢磨合掉的。
这里简单解释一下为什么此时编译,Keil会报大量错误。首先,EtherCAT协议虽然公开、免费,但倍福公司也要赚钱,于是他们自己设计生产了ET1100,ET1200等EtherCAT PHY芯片,同时还有EL9800开发板,并发布了与之配套的SSC软件与之适配。这套玩意儿价格确实很高,淘宝搜ET1100价格基本都在170左右,而且BGA封装的芯片对中小微企业也不太友好。于是MICROCHIP顺应时势,也想分一杯羹,设计制造了同样功能的LAN9252,淘宝售价在50元左右,而且有TQFP封装,中小微企业的福音,电烙铁直接能搞定。同时,为了推广自家的LAN9252,MICROCHIP还很体贴的提供了SSC SDK为大家辅助生成协议栈,但是有点不厚道的是,MICROCHIP为LAN9252提供的SDK,仅支持自家的PIC32单片机,这家伙跟STM32是竞争对手,所以不可能为他人做嫁衣,于是我们目前得到的所有代码,是基于PIC32单片机的,而不是STM32的。所以KEIL编译出来当然一大堆的错误,连MCU芯片型号都不一致。

移植,移植,移植

OK,这下思路明确了,下一步的任务就是要把基于PIC32的代码移植成STM32F1的代码,不要慌,稳住,相信自己一定可以的。
整体思路就是要充分利用KEIL的报错信息,以及Ctrl+F全局搜索,一步一步地去改源代码,我这里没法事无巨细地讲解,因为报错实在太多了,只能选几个典型的示例来做个示范。比如:

function “INTDisableInterrupts” declared implicitly

碰到的第一个错误应该是这个,定位到的代码应该是9252_HW.c的190行:

static void GetInterruptRegister(void)
{
      DISABLE_AL_EVENT_INT;
      HW_EscReadIsr((MEM_ADDR *)&EscALEvent.Word, 0x220, 2);
      ENABLE_AL_EVENT_INT;

}

DISABLE_AL_EVENT_INT这是一个宏定义,看名字应该是关闭所有中断,追踪到同文件第106行:

#define DISABLE_GLOBAL_INT          INTDisableInterrupts()
#define ENABLE_GLOBAL_INT           INTEnableInterrupts()
#define DISABLE_AL_EVENT_INT        DISABLE_GLOBAL_INT
#define ENABLE_AL_EVENT_INT         ENABLE_GLOBAL_INT

于是看到INTDisableInterrupts()INTEnableInterrupts()这两个玩意儿,果然是PIC32中启用和禁用全局中断的函数,对应STM32,应该是下面这段:

#include "stm32f1xx.h"
#define   DISABLE_GLOBAL_INT        __disable_irq()
#define   ENABLE_GLOBAL_INT            __enable_irq()
argument of type “__packed unsigned short “ is incompatible with parameter of type “const void restrict”

这个错误比较隐蔽,是__packed关键字引发的,STM32中,使用__packed关键字可以强制结构体内的数据以1字节对齐(非常不推荐,M3内核通常情况下会保证最高效率进行4字节对齐),在进行一些函数传参时,会报错,意思是无法将一个1字节对齐的结构体指针,强制转成void*,因为void*这种泛型指针,默认是4字节对齐的。
KEIL跟踪这种错误比较难跟,考虑使用Ctrl+F进行全局搜索关键词packed,找到ecat_def.h第742,754行:
packed
可以发现定义了4个宏,分别是两个结构体的前缀和后缀,然后在两个结构体后缀宏上,打上了__attribute__((aligned(1), packed))的标记,看来原作者的意图是将这两个宏对应的结构体变成1字节对齐的数据结构,然而PIC32(或者KEIL内的编译器,没有详细去了解)的字节对齐语法跟STM32的还是有一些差异的,导致字节对齐的语法在这里并不适用,感兴趣的朋友可以去详细了解一下。我们这里就简单的将这个宏后面的映射语句删掉,将其变成空的宏定义。
但是要注意,这两个宏是对应邮箱和过程数据数据结构的,后面设计InputMap和OutputMap时,也要注意,不再是1字节对齐了,而是默认的4字节对齐。

..\EtherCAT\ecatappl.c(143): warning: #1215-D: #warning directive: “Define the timer ticks per ms”

这个报警按照字面上的理解就是有个宏没有定义,我们搜一下这个名为ECAT_TIMER_INC_P_MS的宏。
9252HW.h第146行确实定义了这个宏,但是KEIL帮我自动感应置灰了,因为这个宏定义在一个#ifdef PIC32_HW预编译语法内部,显然我们这里的库函数框架并没有谁会去定义一个PIC32_HW来,因此我们需要将这里面由#ifdef PIC32_HW包围的一大段宏定义,全部复制一份副本,然后用类似#ifdef STM32F1的预编译语法包围起来:
F1
需要说明的是,考虑到本项目定时器配置的具体情况,这里将ECAT_TIMER_INC_P_MS的定义改为与之前配置的1ms定时器完全匹配的数值,即1000,配图中的2000需要修订

其他错误和警告

其他还有若干错误和警告,不胜枚举,这里就不一一赘述了,像什么UINT8,UINT16类型没找到,协议栈需要的数据类型都在ecat_def.h里;参数为空的函数需要加void等小问题,相信聪明的你一定能自己修复。

SPI适配

这里着重讲一下SPI方面的报错处理,把这些报错处理掉,其实就相当于SPI的移植了。主要是SPIDriver.hSPIDriver.c两个文件的去警告和移植。

SPIDriver.h

SPIDriver.h中是相关函数的定义,我们看到:
SPI.H
其中,SPIOpen(),SPIPut()两个函数对于STM32来说无意义,直接删掉,SPIRead(),SPIReadBurstMode()函数参数为空,需要加上void,否则有一个warning。其他就没什么变动了。

SPIDriver.c
  • 打开SPIDriver.c,第一个函数居然是Delay(),这个函数是用PIC32编译器语法写的,显然没用,而且这个函数整个代码都没用到过,有点搞笑,果断删掉。
  • 继而删掉SPIPutSPIOpen两个函数。
  • SPIWriteSPIRead用STM32的常规语法代替:
void SPIWrite(UINT8 data) {
    HAL_SPI_Transmit(&hspi1, &data, 1, 2000);
}

UINT8 SPIRead() {
    UINT8 data;   
    HAL_SPI_Receive(&hspi1, &data, 1, 2000);
    return (data);
}

以上代码用过STM32 SPI通信和HAL库的朋友应该毫无压力。

  • CSLOW()CSHIGH()两个宏的修复:这两个宏是SPI的NSS使能引脚控制,一般我们做SPI通信都会设置成软件使能,这里也不例外,软件使能能够更灵活地控制读写位数。将这两个宏修复如下:
#define DESELECT_SPI    HAL_GPIO_WritePin(SPI1_NSS_GPIO_Port,SPI1_NSS_Pin,GPIO_PIN_SET)
#define SELECT_SPI        HAL_GPIO_WritePin(SPI1_NSS_GPIO_Port,SPI1_NSS_Pin,GPIO_PIN_RESET) 

#define CSHIGH()        DESELECT_SPI
#define CSLOW()            SELECT_SPI

其中SPI1_NSS_GPIO_PortSPI1_NSS_Pin的定义在CubeMX中已经设计好了,存于main.h文件中。

总结

本文讲解了通过CubeMX软件生成STM32工程,同时重新使用LAN9252的SDK,配合SSC软件生成了新的协议栈。将协议栈的相关代码混入工程文件,并尝试一步一步修复代码中的错误和警告。最后单独讲解了SPI部分的代码修复,这部分代码不算复杂,用好Keil的Go to Definition和Ctrl+F全局搜索,一步一步跟踪调试总归能搞定的,调试过程中做到胆大心细,及时保存和备份副本,感觉可以删掉的就大胆删,大不了回档重来。
下一章,我们将全面适配LAN9252,观察EtherCAT协议栈暴露给上层应用的接口,并在我们配置的各个中断中调用它们。