目录
目的:编写一个可以稳定连接到局域网的STM32网络通信程序
硬件和软件:
具体步骤:
1、利用STM32CubeMX建立Keil工程文件
2、在keil中修改代码和配置工程
3、代码烧录、功能验证
1、自制STM32H743XIH6开发板,PHY芯片为DP83848
2、PC一台、路由器一台(可有可无)
补充一点供大家参考:华为、荣耀的路由器好像兼容性很差,我试了很久就是ping不通,后面换了其他品牌的路由器就可以了,一整个大无语。
3、STM32CubeMX6.4.0(或6.0.1或6.6.1)这几个版本我都亲自验证过,6.5.0经测试存在一些问题
4、Keil5.27+JLINK
5、SSCOM串口/网络调试器
(1)选择对应芯片型号,Start Project,建立CubeMX工程
(2)Project Manager-Project菜单下设置文件名,IDE和堆栈大小
Project Manager-Coder Generator菜单下勾选红框处,为每个模块生成新的.c和.h文件
(3)配置系统和总线时钟,首先需要确定STM32芯片的外部晶振的频率,我的是12Mhz,下面以此为例。Pinout&Configuration菜单下,System Core-RCC选项,HSE一般为外部晶振,有源晶振和无源晶振均可配置为Crystal/Ceramic Resonator,LSE不用不需要配置。有Power Regulator Voltage Scale这项参数的需要配置为Scale0,否则无法达到最大时钟。
点击Clock Configuration菜单,左侧Input frequency键入12(以自己硬件情况为准),晶振频率的设定非常重要,如果和实际硬件不匹配,则单片机大概率会无法正常工作。典型情况是串口输出为乱码。
晶振系统时钟源分支处选择HSE,后面分支处选择PLLCLK,再结合芯片最高时钟和实际需求确定系统时钟,系统时钟一般配置为最高频率,H743是480Mhz。在系统频率处键入480,软件会自动求解参数并进行配置,非常方便。
到这里,时钟的配置就完成了。时钟是单片机的“心脏”,请确保其配置准确无误。
(4)回到Pinout&Configuration菜单,配置MPU。System Core-CORTEX_M7选项,开启CPU_DCache,MPU其余功能的配置我们用代码实现。
(5)配置串口用于后续调试。Connectivity-USART1选项,使用异步串口,引脚无脑配置为PA9和PA10,速度保险起见设置为VeryHigh,串口参数115200,8,None,1。
(6)接下来配置本文的主角,以太网模块。Connectivity-ETH选项,主流都是使用RMII接口。
这里的三个地址参数尤其重要,我尝试过很多参数,还是下面的一组最为稳妥,务必按照图片的参数进行配置,这样才可以保证生成的程序能够被ping通。CubeMX6.6.1版本没有Rx Buffers Address这项参数,不用管就行。
最后一个参数影响不大,一般设置1524或1536。根据个人需求设置MAC地址,一般来说默认即可。我这里把最后三位改成了743,表示芯片型号。
GPIO Setting菜单修改RMII接口对应的9个引脚,具体配置以连接到PHY芯片的Pin为准,我这里是把ETH_TXD1默认分配的PG12修改为PB13。Ctrl+A全选引脚,速度配置为VeryHigh,否则无法满足网口通信速率的要求。
(7)RMII接口中不包含复位引脚,还需要单独配置一个ETH_RESET引脚对PHY芯片进行硬件复位。我的单片机通过PH4引脚连接到DP83848的复位引脚,因此在引脚图中选择PH4,配置为GPIO_Output,其余保持默认即可。
(8)最后配置同样重要的LWIP协议栈,Middleware-LWIP选项,开启后选择平台设置,PHY驱动选择LAN8742,如果板子上的PHY芯片型号为LAN8742和LAN8720A,那么便不需要进行对应PHY驱动代码的修改。而DP83848PHY芯片因为寄存器的地址不兼容,需要后面生成代码之后再做相应修改,修改步骤见2-4。
a.如果需要设置静态IP,General Settings菜单下,关闭DHCP,设置单片机的IP地址、网关和子网掩码,注意参数需要和路由器的网关匹配。
b.如果需要开启DHCP,则General Settings菜单下不需要修改,保持默认。另建议在Key Options菜单下勾选高级参数,打开HOSTNAME功能,这样路由器识别到单片机设备后会有固定的名称标识,便于区分。
路由器识别到STM32的设备名默认值为“lwip”,可在ethernetif.c下方代码处自定义设备名称。
为了尽可能的保持工程简单,其余设置保持默认值即可。
(9)好,到这里CubeMX工程就创建完毕了,点击右上角生成代码,进度条走完之后,点击打开工程进入Keil。这里注意路径名不要包含中文,否则不会出现下面的对话框。
(1)首先进行一些基本操作,打开魔术棒对话框
Output取消生成浏览信息,加快代码编译速度
Debug选择自己的烧录器,我这里选择J-LINK,进入Setting,第一次可能会要求选择芯片内核型号,H743属于M7内核,选择Cortex-M7。
Port选择SW,成功识别到芯片。
再进入Flash Download子菜单,勾选Reset and Run。 检查Flash容量是否正确。
(2)此时,编译一下,应该是0Error,但是因为工程尚未完全配置好,所以烧录下去也是无法实现预期功能的,还需要进行进一步的代码修改。我们首先配置串口,一方面串口可以打印信息,用于基本的调试,另一方面也可以验证我们的时钟是否配置正确。
这里我选择不适用Micro Lib,因为再一些大型工程里,使用微库可能会带来意想不到的问题,我一位经验丰富的师兄测试过,在使用微库后,浮点数除法运算的时间会大大增加,所以我一般不使用微库。
main函数添加一行调试信息printf("\r\nSTM32H743 ETH LWIP TEST");
打开usart的源文件,添加串口重定向的代码,如下:
#include "stdio.h"
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
if(USART1 != NULL)
{
//循环发送,直到发送完毕
while((USART1->ISR&0X40)==0)
{
}
USART1->TDR=(uint8_t)ch;
}
return ch;
}
注意添加代码的时候尽量在形如
/* USER CODE BEGIN 1 */
和
/* USER CODE END 1 */
的注释之间,这样之后再次使用CubeMX生成代码的时候,这些代码不会被替换掉。
添加后编译工程并烧录,通过串口调试工具验证串口打印功能是否正常。
如果输出正常,则继续下面的调试,否则检查上面的步骤是否有疏漏。
(3)main函数最前面,增加MPU的配置以使用ETH模块,这里的参数配置参考了其他人的博客。自己尝试修改后,发现功能出现异常,暂时还不了解个中原理。
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct;
/* Disable the MPU */
HAL_MPU_Disable();
/* Configure the MPU attributes as Device not cacheable
for ETH DMA descriptors */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30040000;
MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* Configure the MPU attributes as Cacheable write through
for LwIP RAM heap which contains the Tx buffers */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30044000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* Enables the MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
(4)根据lan8742.c中的示例代码改写得到DP83848的初始化函数和连接状态检测函数,然后进行两个函数的替换。
int32_t DP83848_Init(lan8742_Object_t *pObj);
int32_t DP83848_GetLinkState(lan8742_Object_t *pObj);
两个函数代码先贴在下面,具体原理在文末再分析。
//DP83848的初始化函数
int32_t DP83848_Init(lan8742_Object_t *pObj)
{
uint32_t tickstart = 0, regvalue = 0, addr = 0;
int32_t status = DP83848_STATUS_OK;
if(pObj->Is_Initialized == 0)
{
if(pObj->IO.Init != 0)
{
/* GPIO and Clocks initialization */
pObj->IO.Init();
}
/* Get the device address from special mode register */
for(addr = 0; addr <= DP83848_MAX_DEV_ADDR; addr ++)
{
printf("\r\n扫描地址:0x%02x",addr);
if(pObj->IO.ReadReg(addr, DP83848_PHYCR_RW, ®value) < 0)
{
status = DP83848_STATUS_READ_ERROR;
/* Can't read from this device address
continue with next address */
printf(",失败");
continue;
}
printf(",返回值:0x%02x",regvalue);
if((regvalue & 0x001F) == addr)
{
//存储地址值
pObj->DevAddr = addr;
status = DP83848_STATUS_OK;
printf(",获取PHY地址成功");
break;
}
}
if(pObj->DevAddr > DP83848_MAX_DEV_ADDR)
{
status = DP83848_STATUS_ADDRESS_ERROR;
}
if(pObj->IO.ReadReg(addr, DP83848_BMCR_RW, ®value) < 0)
{
//读取失败
printf("\r\nDP83848_BMCR_RW read fail");
}
else
{
//读取成功
printf("\r\nDP83848_BMCR_RW read ok (0x%X)",regvalue);
}
//SOFT_RESET
regvalue = regvalue|DP83848_BMCR_SOFT_RESET;
if(pObj->IO.WriteReg(addr, DP83848_BMCR_RW, regvalue) < 0)
{
//写入失败
printf("\r\nDP83848_BMCR_RW write fail");
}
else
{
//写入成功
printf("\r\nDP83848_BMCR_RW write ok (0x%X)",regvalue);
}
HAL_Delay(5);
//先读取该寄存器的数据
if(pObj->IO.ReadReg(addr, DP83848_PHYCR_RW, ®value) < 0)
{
//读取失败
printf("\r\nDP83848_PHYCR_RW read fail");
}
else
{
//读取成功
printf("\r\nDP83848_PHYCR_RW read ok (0x%X)",regvalue);
}
//bit5 是LED寄存器,配置成有数据传输的时候就闪烁 2019/06/03
bit_false(regvalue,bit(5));
if(pObj->IO.WriteReg(addr, DP83848_PHYCR_RW, regvalue) < 0)
{
//写入失败
printf("\r\nDP83848_PHYCR_RW write fail");
}
else
{
//写入成功
printf("\r\nDP83848_PHYCR_RW write ok (0x%X)",regvalue);
}
HAL_Delay(5);
//再读取该寄存器的数据,看之前的设置有没有生效
if(pObj->IO.ReadReg(addr, DP83848_PHYCR_RW, ®value) < 0)
{
//读取失败
printf("\r\nDP83848_PHYCR_RW read fail");
}
else
{
//读取成功
printf("\r\nDP83848_PHYCR_RW read ok (0x%X)",regvalue);
}
DP83848_RESET:
if(pObj->IO.ReadReg(addr, DP83848_PHYSTS_RO, ®value) >= 0)
{
printf("PHY状态寄存器:PHYSTS status=0x%x\n", regvalue);
if( (regvalue&0x0011) != 0x0011)
{
printf("\r\nDP83848初始化还未完成,等待中...");
HAL_Delay(200);
goto DP83848_RESET;
}
else
{
printf("\r\nDP83848初始化成功!");
}
}
}
if(status == DP83848_STATUS_OK)
{
tickstart = pObj->IO.GetTick();
/* Wait for 2s to perform initialization */
while((pObj->IO.GetTick() - tickstart) <= DP83848_INIT_TO)
{
}
pObj->Is_Initialized = 1;
}
return status;
}
//DP83848网络连接状态检测函数
int32_t DP83848_GetLinkState(lan8742_Object_t *pObj)
{
uint32_t readval = 0;
/* Read Status register */
if(pObj->IO.ReadReg(pObj->DevAddr, DP83848_BMSR_RO, &readval) < 0)
{
return DP83848_STATUS_READ_ERROR;
}
/* Read Status register again */
if(pObj->IO.ReadReg(pObj->DevAddr, DP83848_BMSR_RO, &readval) < 0)
{
return DP83848_STATUS_READ_ERROR;
}
if((readval & DP83848_BMSR_LINK_STATUS) == 0)
{
/* Return Link Down status */
return DP83848_STATUS_LINK_DOWN;
}
/* Check Auto negotiaition */
if(pObj->IO.ReadReg(pObj->DevAddr, DP83848_BMCR_RW, &readval) < 0)
{
return DP83848_STATUS_READ_ERROR;
}
if((readval & DP83848_BMCR_AUTONEGO_EN) != DP83848_BMCR_AUTONEGO_EN)
{
if(((readval & DP83848_BMCR_SPEED_SELECT) == DP83848_BMCR_SPEED_SELECT) && ((readval & DP83848_BMCR_DUPLEX_MODE) == DP83848_BMCR_DUPLEX_MODE))
{
return DP83848_STATUS_100MBITS_FULLDUPLEX;
}
else if ((readval & DP83848_BMCR_SPEED_SELECT) == DP83848_BMCR_SPEED_SELECT)
{
return DP83848_STATUS_100MBITS_HALFDUPLEX;
}
else if ((readval & DP83848_BMCR_DUPLEX_MODE) == DP83848_BMCR_DUPLEX_MODE)
{
return DP83848_STATUS_10MBITS_FULLDUPLEX;
}
else
{
return DP83848_STATUS_10MBITS_HALFDUPLEX;
}
}
else /* Auto Nego enabled */
{
if(pObj->IO.ReadReg(pObj->DevAddr, DP83848_PHYSTS_RO, &readval) < 0)
{
return DP83848_STATUS_READ_ERROR;
}
/* Check if auto nego not done */
if((readval & DP83848_STATUS_AUTONEGO_NOTDONE) == 0)
{
return DP83848_STATUS_READ_ERROR;
}
if((readval & DP83848_PHYSTS_HCDSPEEDMASK) == DP83848_PHYSTS_100BTX_FD)
{
return DP83848_STATUS_100MBITS_FULLDUPLEX;
}
else if ((readval & DP83848_PHYSTS_HCDSPEEDMASK) == DP83848_PHYSTS_100BTX_HD)
{
return DP83848_STATUS_100MBITS_HALFDUPLEX;
}
else if ((readval & DP83848_PHYSTS_HCDSPEEDMASK) == DP83848_PHYSTS_10BT_FD)
{
return DP83848_STATUS_10MBITS_FULLDUPLEX;
}
else if ((readval & DP83848_PHYSTS_HCDSPEEDMASK) == DP83848_PHYSTS_10BT_HD)
{
return DP83848_STATUS_10MBITS_HALFDUPLEX;
}
}
}
运行上面两个函数用到的宏定义如下:
#include "lan8742.h"
#define DP83848_BMCR_RW ((uint16_t)0x0000U)
#define DP83848_BMSR_RO ((uint16_t)0x0001U)
#define DP83848_PHYSTS_RO ((uint16_t)0x0010U)
#define DP83848_PHYCR_RW ((uint16_t)0x0019U)
#define DP83848_BMSR_LINK_STATUS ((uint16_t)0x0004U)
#define DP83848_BMCR_SOFT_RESET ((uint16_t)0x8000U)
#define DP83848_BMCR_LOOPBACK ((uint16_t)0x4000U)
#define DP83848_BMCR_SPEED_SELECT ((uint16_t)0x2000U)
#define DP83848_BMCR_AUTONEGO_EN ((uint16_t)0x1000U)
#define DP83848_BMCR_POWER_DOWN ((uint16_t)0x0800U)
#define DP83848_BMCR_ISOLATE ((uint16_t)0x0400U)
#define DP83848_BMCR_RESTART_AUTONEGO ((uint16_t)0x0200U)
#define DP83848_BMCR_DUPLEX_MODE ((uint16_t)0x0100U)
#define DP83848_STATUS_READ_ERROR ((int32_t)-5)
#define DP83848_STATUS_WRITE_ERROR ((int32_t)-4)
#define DP83848_STATUS_ADDRESS_ERROR ((int32_t)-3)
#define DP83848_STATUS_RESET_TIMEOUT ((int32_t)-2)
#define DP83848_STATUS_ERROR ((int32_t)-1)
#define DP83848_STATUS_OK ((int32_t) 0)
#define DP83848_STATUS_LINK_DOWN ((int32_t) 1)
#define DP83848_STATUS_100MBITS_FULLDUPLEX ((int32_t) 2)
#define DP83848_STATUS_100MBITS_HALFDUPLEX ((int32_t) 3)
#define DP83848_STATUS_10MBITS_FULLDUPLEX ((int32_t) 4)
#define DP83848_STATUS_10MBITS_HALFDUPLEX ((int32_t) 5)
#define DP83848_STATUS_AUTONEGO_NOTDONE ((int32_t) 6)
#define DP83848_PHYSTS_AUTONEGO_DONE ((uint16_t)0x0010U)
#define DP83848_PHYSTS_HCDSPEEDMASK ((uint16_t)0x0006U)
#define DP83848_PHYSTS_10BT_HD ((uint16_t)0x0002U)
#define DP83848_PHYSTS_10BT_FD ((uint16_t)0x0006U)
#define DP83848_PHYSTS_100BTX_HD ((uint16_t)0x0000U)
#define DP83848_PHYSTS_100BTX_FD ((uint16_t)0x0004U)
#define DP83848_INIT_TO ((uint32_t)2000U)
#define DP83848_MAX_DEV_ADDR ((uint32_t)31U)
#define bit(n) (1 << n)
#define bit_false(x,mask) (x) &= ~(mask)
int32_t DP83848_Init(lan8742_Object_t *pObj);
int32_t DP83848_GetLinkState(lan8742_Object_t *pObj);
将代码放置在合适的位置,我直接放在main函数下面了。添加两个函数后,
DP83848_Init(&LAN8742)替换ethernetif.c文件中的LAN8742_Init(&LAN8742)
DP83848_GetLinkState(&LAN8742)替换ethernetif.c文件中的LAN8742_GetLinkState(&LAN8742)
各一处,替换完成后,记得确认相应头文件是否包含。
(5)在LAN8742_RegisterBusIO(&LAN8742, &LAN8742_IOCtx);语句前添加PHY的硬件复位语句。
HAL_GPIO_WritePin(GPIOH,GPIO_PIN_4,GPIO_PIN_SET);
HAL_Delay(5);
HAL_GPIO_WritePin(GPIOH,GPIO_PIN_4,GPIO_PIN_RESET);
HAL_Delay(5);
HAL_GPIO_WritePin(GPIOH,GPIO_PIN_4,GPIO_PIN_SET);
HAL_Delay(5);
最后在main函数while(1)循环中添加MX_LWIP_Process();语句,用来完成网口通讯的基本功能。
这样,整个工程就算搭建完毕了,编译排查剩余的错误。编译成功后,可以连接设备进行最终的验证了。
如果你手头有可用的路由器,那就用网线连接单片机的网口和路由器的LAN口,PC同样连接到路由器的LAN口或者连接路由器的无线,确保PC和STM32在同一个局域网下的同一个网段。然后烧录、运行程序,若在串口工具上看到类似的输出,那么恭喜你离成功不远了!
需要注意的事,我提供的DP83848的初始化代码逻辑上有些问题,必须要先用网线连接到路由器或者电脑之后才能初始化成功,否则会处于while死循环中。
如果没有路由器,可以用网线连接单片机的网口和电脑的网口,设置PC的IP地址为静态地址,同样保证PC和STM32在同一个局域网下的同一个网段。例如,本文STM32的IP地址为192.168.1.110,那么PC的ipv4设置为IP:192.168.1.1,子网掩码:255.255.255.0,网关192.168.1.1。
完成上述步骤后,在电脑上Win+R,键入cmd打开控制台,ping 192.168.1.110(以CubeMX中你配置的IP地址为准),会发现已经能ping通了!
此时也可以在路由器的后台,看到我们创建的这个基于STM32的终端设备了。检查下IP地址和MAC地址,都是没有问题的的。
我这里测试的,只要STM32上电初始化完成,路由器几秒之后就可以100%识别到该终端设备。如果没有成功,可以检查网络连接情况,多ping几次,还不行的话就要检查STM32的代码了。
1、上面的工程搭建方案有一个非常好的优点:如果后面需要在CubeMX中更改IP地址等参数,那么重新生成代码后,可以最大程度的保留用户代码。如果使用的是DP83848PHY芯片,使用CubeMX修改参数重新生成代码后,只需要重新替换
DP83848_Init(&LAN8742)和
DP83848_GetLinkState(&LAN8742)
两个函数即可再次编译烧录程序。如果使用的是LAN8742PHY,则不需要进行任何操作。使用CubeMX修改参数并重新生成代码后,直接编译也不会报错。
2、经过大量测试,发现这个工程仍然存在一些问题,稳定性不是特别好。单片机直连PC,PC可以ping通。单片机经过路由器连接PC的话需要多ping几次才能ping通。
3、单片机直连PC,丢包率为0;但单片机经过路由器连接PC,多次ping会有一定的丢包率,设置成中断模式现象依旧,怀疑是路由器中转能力不够或者无线信号不稳定的问题?
前前后后调了三天的时间,各种版本的CubeMX试了个遍,走了很多弯路,所以把这次成功的调试记录下来供大家参考,欢迎大家一起交流。
H743_ETH_LWIP.zip
【LWIP】(补充)STM32H743(M7内核)CubeMX配置LWIP并ping通_芜~湖~的博客-CSDN博客
DP83848J datasheet
LAN8720A datasheet,复制链接到浏览器