基于STM32CubeMX创建的STM32H743+DP83848+LWIP网络通信程序调试_20221127算是胎教级教程了

目录

目的:编写一个可以稳定连接到局域网的STM32网络通信程序

硬件和软件:

具体步骤:

1、利用STM32CubeMX建立Keil工程文件

 2、在keil中修改代码和配置工程

3、代码烧录、功能验证


目的:编写一个可以稳定连接到局域网的STM32网络通信程序

硬件和软件:

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、利用STM32CubeMX建立Keil工程文件

(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地址、网关和子网掩码,注意参数需要和路由器的网关匹配。

静态IP设置

 b.如果需要开启DHCP,则General Settings菜单下不需要修改,保持默认。另建议在Key Options菜单下勾选高级参数,打开HOSTNAME功能,这样路由器识别到单片机设备后会有固定的名称标识,便于区分。

DHCP设置

 路由器识别到STM32的设备名默认值为“lwip”,可在ethernetif.c下方代码处自定义设备名称。

 为了尽可能的保持工程简单,其余设置保持默认值即可。

 (9)好,到这里CubeMX工程就创建完毕了,点击右上角生成代码,进度条走完之后,点击打开工程进入Keil。这里注意路径名不要包含中文,否则不会出现下面的对话框。

 2、在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, &regvalue) < 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, &regvalue) < 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, &regvalue) < 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, &regvalue) < 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, &regvalue) >= 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();语句,用来完成网口通讯的基本功能。

这样,整个工程就算搭建完毕了,编译排查剩余的错误。编译成功后,可以连接设备进行最终的验证了。

3、代码烧录、功能验证

如果你手头有可用的路由器,那就用网线连接单片机的网口和路由器的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,复制链接到浏览器

物联沃分享整理
物联沃-IOTWORD物联网 » 基于STM32CubeMX创建的STM32H743+DP83848+LWIP网络通信程序调试_20221127算是胎教级教程了

发表评论