2024年1715535584,2024年最新为什么物联网嵌入式开发能最好地改变物联网嵌入式开发

收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
img
img

如果你需要这些资料,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

39.8.1 硬件设计

在讲解移植步骤之前,有必须先介绍我们的实验硬件设计,主要是LAN8720A通过RMII和SMI接口与STM32F42x控制器连接,见图 3914。

图 3914 PHY硬件设计

电路设计时,将NINTSEL引脚通过下拉电阻拉低,设置NINT/FEFCLKO为输出50MHz时钟,当然前提是在XTAL1和XTAL2接入了25MHz的时钟源。另外也把REGOFF引脚通过下拉电阻拉低,使能使用内部+1.2V稳压器。

39.8.2 移植步骤

之前已经介绍了LwIP源代码(lwip-1.4.1.zip)和ST官方LwIP测试平台资料(stsw-stm32070.zip)下载,我们移植步骤是基于这两份资料进行的。

无操作系统移植LwIP需要的文件参考图 3915,图中只显示了*.c文件,还需要用到对应的*.h文件。

图 3915 LwIP移植实验文件结构

接下来,我们就根据图中文件结构详解移植过程。实验例程有需要用到系统滴答定时器systick、调试串口USART、独立按键KEY、LED灯功能,对这些功能实现不做具体介绍,可以参考相关章节理解。

第一步:相关文件拷贝

首先,解压lwip-1.4.1.zip和stsw-stm32070.zip两个压缩包,把整个lwip-1.4.1文件夹拷贝到USER文件夹下,特别说明,在整个移植过程中,不会对lwip-1.4.1.zip文件下的文件内容进行修改。然后,在stsw-stm32070文件夹找到port文件夹(路径:… \Utilities\Third_Party\lwip-1.4.1\port),把整个port文件夹拷贝lwip-1.4.1文件夹中,在port文件夹下的STM32F4x7文件中把arch和Standalone两个文件夹直接剪切到port文件夹中,即此时port文件夹有三个STM32F4x7、arch和Standalone文件夹,最后把STM32F4x7文件夹删除,最终的文件结构见图 3916,arch存放与开发平台相关头文件,Standalone文件夹是无操作系统移植时ETH外设与LwIP连接的底层驱动函数。

图 3916 LwIP相关文件拷贝

lwip-1.4.1文件夹下的doc文件夹存放LwIP版权、移植、使用等等说明文件,移植之前有必须认真浏览一遍;src文件夹存放LwIP的实现代码,也是我们工程代码真正需要的文件;test文件夹存放LwIP部分功能测试例程;另外,还有一些无后缀名的文件,都是一些说明性文件,可用记事本直接打开浏览。port文件夹存放LwIP与STM32平台连接的相关文件,正如上面所说contrib-1.4.1.zip包含了不同平台移植代码,不过遗憾地是没有STM32平台的,所以我们需要从ST官方提供的测试平台找到这部分连接代码,也就是port文件夹的内容。

接下来,在Bsp文件下新建一个ETH文件夹,用于存放与ETH相关驱动文件,包括两个部分文件,其中一个是ETH外设驱动文件,在stsw-stm32070文件夹中找到stm32f4x7_eth.h和stm32f4x7_eth.c两个文件(路径:…\Libraries\STM32F4x7_ETH_Driver),将这两个文件拷贝到ETH文件夹中,对应改名为stm32f429_eth.h和stm32f429_eth.c,这两个文件是ETH驱动文件,类似标准库中外设驱动代码实现文件,在移植过程中我们几乎不过文件的内容。这部分函数由port文件夹相关代码调用。另外一部分是相关GPIO初始化、ETH外设初始化、PHY状态获取等等函数的实现,在stsw-stm32070文件夹中找到stm32f4x7_eth_bsp.c、stm32f4x7_eth_bsp.h和stm32f4x7_eth_conf.h三个文件(路径:…\Project\Standalone\tcp_echo_client),将这三个文件拷贝到ETH文件夹中,对应改名为stm32f429_phy.c、stm32f429_phy.h和stm32f429_eth_conf.h。因为,ST官方LwIP测试平台使用的PHY型号不是使用LAN8720A,所以这三个文件需要我们进行修改。

最后,是LwIP测试代码实现,为测试LwIP移植是否成功和检查LwIP功能,我们编写TCP通信实现代码,设置开发板为TCP从机,电脑端为TCP主机。在stsw-stm32070文件夹中找到netconf.c、tcp_echoclient.c、lwipopts.h、netconf.h和tcp_echoclient.h五个文件(路径:…\Project\Standalone\tcp_echo_client),直接拷贝到App文件夹(自己新建)中,netconf.c文件代码实现LwIP初始化函数、周期调用函数、DHCP功能函数等等,tcp_echoclient.c文件实现TCP通信参数代码,lwipopts.h包含LwIP功能选项。

第二部:为工程添加文件

第一步已经把相关的文件拷贝到对应的文件夹中,接下来就可以把需要用到的文件添加到工程中。图 3915已经指示出来工程需要用到的*.c文件,所以最终工程文件结构见图 3917,图中api、ipv4和core都包含了对应文件夹下的所有*.c文件。

图 3917 工程文件结构

接下来,还需要在工程选择中添加相关头文件路径,参考图 3918。

图 3918 添加相关头文件路径

第三步:文件修改

ethernetif.c文件是无操作系统时网络接口函数,该文件在移植是只需修改相关头文件名,函数实现部分无需修改。该文件主要有三个部分函数,一个是low_level_init,用于初始化MAC相关工作环境、初始化DMA描述符链表,并使能MAC和DMA;一个是low_level_output,它是最底层发送一帧数据函数;最后一个是low_level_input,它是最底层接收一帧数据函数。

stm32f429_eth.c和stm32f429_eth.h两个文件用于ETH驱动函数实现,它是通过直接操作寄存器方式实现,这两个文件我们无需修改。stm32f429_eth_conf.h文件包含了一些功能选项的宏定义,我们对部分内容进行了修改。

代码清单 392 stm32f429_eth_conf.h文件宏定义

1 #ifdef USE_Delay

2 #include “Bsp/systick/bsp_SysTick.h”

3 #define _eth_delay_ Delay_10ms

4 #else

5 #define _eth_delay_ ETH_Delay

6 #endif

7

8 #ifdef USE_Delay

9 /* LAN8742A Reset delay */

10 #define LAN8742A_RESET_DELAY ((uint32_t)0x00000005)

11 #else

12 /* LAN8742A Reset delay */

13 #define LAN8742A_RESET_DELAY ((uint32_t)0x00FFFFFF)

14 #endif

15

16 /* The LAN8742A PHY status register */

17 /* PHY status register Offset */

18 #define PHY_SR ((uint16_t)0x001F)

19 /* PHY Speed mask 1:10Mb/s 0:100Mb/s*/

20 #define PHY_SPEED_STATUS ((uint16_t)0x0004)

21 /* PHY Duplex mask 1:Full duplex 0:Half duplex*/

22 #define PHY_DUPLEX_STATUS ((uint16_t)0x0010)

通过宏定义USE_Delay可选是否使用自定义的延时函数,Delay_10ms函数是通过系统滴答定时器实现的延时函数,ETH_Delay函数是ETH驱动自带的简单循环延时函数,延时函数实现方法不同,对形参要求不同。因为ST官方例程是基于DP83848型号的PHY,而开发板的PHY型号是LAN8720A。LAN8720A复位时需要一段延时时间,这里需要定义延时时间长度,大约50ms。驱动代码中需要获取PHY的速度和工作模式,LAN8720A的R31是特殊控制/状态寄存器,包括指示以太网速度和工作模式的状态位。

stm32f42x_phy.c和stm32f42x_phy.h两个文件是ETH外设相关的底层配置,包括RMII接口GPIO初始化、SMI接口GPIO初始化、MAC控制器工作环境配置,还有一些PHY的状态获取和控制修改函数。ST官方例程文件包含了中断引脚的相关配置,主要用于指示接收到以太网帧,我们这里不需要使用,采用无限轮询方法检测接收状态。stm32f42x_phy.h文件存放相关宏定义,包含RMII和SMI引脚信息等宏定义,其中要特别说明的有一个宏,定义了PHY地址:ETHERNET_PHY_ADDRESS,这里根据硬件设计设置为0x00,这在SMI通信是非常重要的。

代码清单 393 ETH_GPIO_Config函数

1 void ETH_GPIO_Config(void)

2 {

3 GPIO_InitTypeDef GPIO_InitStructure;

4

5 /* Enable GPIOs clocks */

6 RCC_AHB1PeriphClockCmd(ETH_MDIO_GPIO_CLK | ETH_MDC_GPIO_CLK |

7 ETH_RMII_REF_CLK_GPIO_CLK|ETH_RMII_CRS_DV_GPIO_CLK|

8 ETH_RMII_RXD0_GPIO_CLK | ETH_RMII_RXD1_GPIO_CLK |

9 ETH_RMII_TX_EN_GPIO_CLK | ETH_RMII_TXD0_GPIO_CLK |

10 ETH_RMII_TXD1_GPIO_CLK | ETH_NRST_GPIO_CLK, ENABLE);

11

12 /* Enable SYSCFG clock */

13 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

14

15 /* MII/RMII Media interface selection ------------------------------*/

16 #ifdef MII_MODE /* Mode MII with STM324xx-EVAL */

17 #ifdef PHY_CLOCK_MCO

18 /* Output HSE clock (25MHz) on MCO pin (PA8) to clock the PHY */

19 RCC_MCO1Config(RCC_MCO1Source_HSE, RCC_MCO1Div_1);

20 #endif /* PHY_CLOCK_MCO */

21

22 SYSCFG_ETH_MediaInterfaceConfig(SYSCFG_ETH_MediaInterface_MII);

23 #elif defined RMII_MODE /* Mode RMII with STM324xx-EVAL */

24

25 SYSCFG_ETH_MediaInterfaceConfig(SYSCFG_ETH_MediaInterface_RMII);

26 #endif

27

28 /* Ethernet pins configuration *************************************/

29 /*

30 ETH_MDIO -------------------------> PA2

31 ETH_MDC --------------------------> PC1

32 ETH_MII_RX_CLK/ETH_RMII_REF_CLK—> PA1

33 ETH_MII_RX_DV/ETH_RMII_CRS_DV ----> PA7

34 ETH_MII_RXD0/ETH_RMII_RXD0 -------> PC4

35 ETH_MII_RXD1/ETH_RMII_RXD1 -------> PC5

36 ETH_MII_TX_EN/ETH_RMII_TX_EN -----> PB11

37 ETH_MII_TXD0/ETH_RMII_TXD0 -------> PG13

38 ETH_MII_TXD1/ETH_RMII_TXD1 -------> PG14

39 ETH_NRST -------------------------> PI1

40 */

41 GPIO_InitStructure.GPIO_Pin = ETH_NRST_PIN;

42 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;

43 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

44 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;

45 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;

46 GPIO_Init(ETH_NRST_PORT, &GPIO_InitStructure);

47

48 ETH_NRST_PIN_LOW();

49 _eth_delay_(LAN8742A_RESET_DELAY);

50 ETH_NRST_PIN_HIGH();

51 _eth_delay_(LAN8742A_RESET_DELAY);

52

53 /* Configure ETH_MDIO */

54 GPIO_InitStructure.GPIO_Pin = ETH_MDIO_PIN;

55 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;

56 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;

57 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;

58 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

59 GPIO_Init(ETH_MDIO_PORT, &GPIO_InitStructure);

60 GPIO_PinAFConfig(ETH_MDIO_PORT, ETH_MDIO_SOURCE, ETH_MDIO_AF);

61

62 /* Configure ETH_MDC */

63 GPIO_InitStructure.GPIO_Pin = ETH_MDC_PIN;

64 GPIO_Init(ETH_MDC_PORT, &GPIO_InitStructure);

65 GPIO_PinAFConfig(ETH_MDC_PORT, ETH_MDC_SOURCE, ETH_MDC_AF);

66

67 /**************************************/

68 /** 省略部分引脚初始化 ***/

69 /**************************************/

70

71 /* Configure ETH_RMII_TXD1 */

72 GPIO_InitStructure.GPIO_Pin = ETH_RMII_TXD1_PIN;

73 GPIO_Init(ETH_RMII_TXD1_PORT, &GPIO_InitStructure);

74 GPIO_PinAFConfig(ETH_RMII_TXD1_PORT, ETH_RMII_TXD1_SOURCE,

75 ETH_RMII_TXD1_AF);

76 }

STM32f42x控制器支持MII和RMII接口,通过程序控制使用RMII接口,同时需要使能SYSYCFG时钟,函数后部分就是接口GPIO初始化实现,这里我们还连接了LAN8720A的复位引脚,通过拉低一段时间让芯片硬件复位。

代码清单 394 ETH_MACDMA_Config函数

1 static void ETH_MACDMA_Config(void)

2 {

3 /* Enable ETHERNET clock */

4 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC |

5 RCC_AHB1Periph_ETH_MAC_Tx|RCC_AHB1Periph_ETH_MAC_Rx,ENABLE);

6

7 /* Reset ETHERNET on AHB Bus */

8 ETH_DeInit();

9 /* Software reset */

10 ETH_SoftwareReset();

11 /* Wait for software reset */

12 while (ETH_GetSoftwareResetStatus() == SET);

13

14 /* ETHERNET Configuration ------------------------------*/

15 /* 缺省配置ETH_InitStructure */

16 ETH_StructInit(&ETH_InitStructure);

17

18 /* Fill ETH_InitStructure parametrs */

19 /*-------------------- MAC ----------------------------*/

20 /* 开启网络自适应功能,速度和工作模式无需配置 */

21 ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Enable;

22 // ETH_InitStructure.ETH_AutoNegotiation = ETH_AutoNegotiation_Disable;

23 // ETH_InitStructure.ETH_Speed = ETH_Speed_10M;

24 // ETH_InitStructure.ETH_Mode = ETH_Mode_FullDuplex;

25 /* 关闭反馈 */

26 ETH_InitStructure.ETH_LoopbackMode = ETH_LoopbackMode_Disable;

27 /* 关闭重传功能 */

28 ETH_InitStructure.ETH_RetryTransmission=ETH_RetryTransmission_Disable;

29 /* 关闭自动去除PDA/CRC功能 */

30 ETH_InitStructure.ETH_AutomaticPadCRCStrip =

31 ETH_AutomaticPadCRCStrip_Disable;

32 /* 关闭接收所有的帧 */

33 ETH_InitStructure.ETH_ReceiveAll = ETH_ReceiveAll_Disable;

34 /* 允许接收所有广播帧 */

35 ETH_InitStructure.ETH_BroadcastFramesReception =

36 ETH_BroadcastFramesReception_Enable;

37 /* 关闭混合模式的地址过滤 */

38 ETH_InitStructure.ETH_PromiscuousMode = ETH_PromiscuousMode_Disable;

39 /* 对于组播地址使用完美地址过滤 */

40 ETH_InitStructure.ETH_MulticastFramesFilter =

41 ETH_MulticastFramesFilter_Perfect;

42 /* 对单播地址使用完美地址过滤 */

43 ETH_InitStructure.ETH_UnicastFramesFilter =

44 ETH_UnicastFramesFilter_Perfect;

45 #ifdef CHECKSUM_BY_HARDWARE

46 /* 开启ipv4和TCP/UDP/ICMP的帧校验和卸载 */

47 ETH_InitStructure.ETH_ChecksumOffload = ETH_ChecksumOffload_Enable;

48 #endif

49

50 /*------------------------ DMA -------------------------------*/

51 /*当我们使用帧校验和卸载功能的时候,一定要使能存储转发模式,存储

52 转发模式中要保证整个帧存储在FIFO中, 这样MAC能插入/识别出帧校验

53 值,当真校验正确的时候DMA就可以处理帧,否则就丢弃掉该帧*/

54 /* 开启丢弃TCP/IP错误帧 */

55 ETH_InitStructure.ETH_DropTCPIPChecksumErrorFrame =

56 ETH_DropTCPIPChecksumErrorFrame_Enable;

57 /* 开启接收数据的存储转发模式 */

58 ETH_InitStructure.ETH_ReceiveStoreForward =

59 ETH_ReceiveStoreForward_Enable;

60 /* 开启发送数据的存储转发模式 */

61 ETH_InitStructure.ETH_TransmitStoreForward =

62 ETH_TransmitStoreForward_Enable;

63

64 /* 禁止转发错误帧 */

65 ETH_InitStructure.ETH_ForwardErrorFrames =

66 ETH_ForwardErrorFrames_Disable;

67 /* 不转发过小的好帧 */

68 ETH_InitStructure.ETH_ForwardUndersizedGoodFrames =

69 ETH_ForwardUndersizedGoodFrames_Disable;

70 /* 打开处理第二帧功能 */

71 ETH_InitStructure.ETH_SecondFrameOperate =

72 ETH_SecondFrameOperate_Enable;

73 /* 开启DMA传输的地址对齐功能 */

74 ETH_InitStructure.ETH_AddressAlignedBeats =

75 ETH_AddressAlignedBeats_Enable;

76 /* 开启固定突发功能 */

77 ETH_InitStructure.ETH_FixedBurst = ETH_FixedBurst_Enable;

78 /* DMA发送的最大突发长度为32个节拍 */

79 ETH_InitStructure.ETH_RxDMABurstLength = ETH_RxDMABurstLength_32Beat;

80 /*DMA接收的最大突发长度为32个节拍 */

81 ETH_InitStructure.ETH_TxDMABurstLength = ETH_TxDMABurstLength_32Beat;

82 ETH_InitStructure.ETH_DMAArbitration =

83 ETH_DMAArbitration_RoundRobin_RxTx_2_1;

84

85 /* 配置ETH */

86 EthStatus = ETH_Init(&ETH_InitStructure, ETHERNET_PHY_ADDRESS);

87 }

首先是使能ETH时钟,复位ETH配置。ETH_StructInit函数用于初始化ETH_InitTypeDef结构体变量,会给每个成员赋予缺省值。接下来就是根据需要配置ETH_InitTypeDef结构体变量,关于结构体各个成员意义已在"ETH初始化结构体详解"作了分析。最后调用ETH_Init函数完成配置,ETH_Init函数有两个形参,一个是ETH_InitTypeDef结构体变量指针,第二个是PHY地址,函数还有一个返回值,用于指示初始化配置是否成功。

代码清单 395 ETH_BSP_Config函数

1 #define GET_PHY_LINK_STATUS()

2 (ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS,PHY_BSR)&0x00000004)

3

4 void ETH_BSP_Config(void)

5 {

6 /* Configure the GPIO ports for ethernet pins */

7 ETH_GPIO_Config();

8

9 /* Configure the Ethernet MAC/DMA */

10 ETH_MACDMA_Config();

11

12 /* Get Ethernet link status*/

13 if (GET_PHY_LINK_STATUS()) {

14 EthStatus |= ETH_LINK_FLAG;

15 }

16 }

GET_PHY_LINK_STATUS()是定义获取PHY链路状态的宏,如果PHY连接正常那么整个宏定义为1,如果不正常则为0,它是通过ETH_ReadPHYRegister函数读取PHY的基本状态寄存器(PHY_BSR)并检测其Link Status位得到的。

ETH_BSP_Config函数分别调用ETH_GPIO_Config和ETH_MACDMA_Config函数完成ETH初始化配置,最后调用GET_PHY_LINK_STATUS()来判断PHY状态,并保存在EthStatus变量中。ETH_BSP_Config函数一般在main函数中优先LwIP_Init函数调用。

代码清单 396 ETH_CheckLinkStatus函数

1 void ETH_CheckLinkStatus(uint16_t PHYAddress)

2 {

3 static uint8_t status = 0;

4 uint32_t t = GET_PHY_LINK_STATUS();

5

6 /* If we have link and previous check was not yet */

7 if (t && !status) {

8 /* Set link up */

9 netif_set_link_up(&gnetif);

10

11 status = 1;

12 }

13 /* If we don’t have link and it was on previous check */

14 if (!t && status) {

15 EthLinkStatus = 1;

16 /* Set link down */

17 netif_set_link_down(&gnetif);

18

19 status = 0;

20 }

21 }

ETH_CheckLinkStatus函数用于获取PHY状态,实际上也是通过宏定义GET_PHY_LINK_STATUS()获取得到的,函数还根据PHY状态通知LwIP当前链路状态,gnetif是一个netif结构体类型变量,LwIP定义了netif结构体类型,用于指示某一网卡相关信息,LwIP是支持多个网卡设备,使用时需要为每个网卡设备定义一个netif类型变量。无操作系统时ETH_CheckLinkStatus函数被无限循环调用。

代码清单 397 ETH_link_callback函数

1 void ETH_link_callback(struct netif *netif)

2 {

3 __IO uint32_t timeout = 0;

4 uint32_t tmpreg;

5 uint16_t RegValue;

6 struct ip_addr ipaddr;

7 struct ip_addr netmask;

8 struct ip_addr gw;

9

10 if (netif_is_link_up(netif)) {

11 /* Restart the auto-negotiation */

12 if (ETH_InitStructure.ETH_AutoNegotiation !=

13 ETH_AutoNegotiation_Disable) {

14 /* Reset Timeout counter */

15 timeout = 0;

16 /* Enable auto-negotiation */

17 ETH_WritePHYRegister(ETHERNET_PHY_ADDRESS, PHY_BCR,

18 PHY_AutoNegotiation);

19 /* Wait until the auto-negotiation will be completed */

20 do {

21 timeout++;

22 } while (!(ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS, PHY_BSR)

23 &PHY_AutoNego_Complete)&&(timeout<(uint32_t)PHY_READ_TO));

24

25 /* Reset Timeout counter */

26 timeout = 0;

27 /* Read the result of the auto-negotiation */

28 RegValue = ETH_ReadPHYRegister(ETHERNET_PHY_ADDRESS, PHY_SR);

29

30 if ((RegValue & PHY_DUPLEX_STATUS) != (uint16_t)RESET) {

31 ETH_InitStructure.ETH_Mode = ETH_Mode_FullDuplex;

32 } else {

33 ETH_InitStructure.ETH_Mode = ETH_Mode_HalfDuplex;

34 }

35 if (RegValue & PHY_SPEED_STATUS) {

36 /* Set Ethernet speed to 10M following the auto-negotiation */

37 ETH_InitStructure.ETH_Speed = ETH_Speed_10M;

38 } else {

39 /* Set Ethernet speed to 100M following the auto-negotiation */

40 ETH_InitStructure.ETH_Speed = ETH_Speed_100M;

41 }

42

43 /*------------ ETHERNET MACCR Re-Configuration -------------*/

44 /* Get the ETHERNET MACCR value */

45 tmpreg = ETH->MACCR;

46

47 /* Set the FES bit according to ETH_Speed value */

48 /* Set the DM bit according to ETH_Mode value */

49 tmpreg |= (uint32_t)(ETH_InitStructure.ETH_Speed |

50 ETH_InitStructure.ETH_Mode);

51

52 /* Write to ETHERNET MACCR */

53 ETH->MACCR = (uint32_t)tmpreg;

54

55 _eth_delay_(ETH_REG_WRITE_DELAY);

56 tmpreg = ETH->MACCR;

57 ETH->MACCR = tmpreg;

58 }

59

60 /* Restart MAC interface */

61 ETH_Start();

62

63 #ifdef USE_DHCP

64 ipaddr.addr = 0;

65 netmask.addr = 0;

66 gw.addr = 0;

67 DHCP_state = DHCP_START;

68 #else

69 IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);

70 IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1 ,

71 NETMASK_ADDR2, NETMASK_ADDR3);

72 IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);

73 #endif /* USE_DHCP */

74

75 netif_set_addr(&gnetif, &ipaddr , &netmask, &gw);

76

77 /* When the netif is fully configured this function must be called.*/

78 netif_set_up(&gnetif);

79

80 EthLinkStatus = 0;

81 } else {

82 ETH_Stop();

83 #ifdef USE_DHCP

84 DHCP_state = DHCP_LINK_DOWN;

85 dhcp_stop(netif);

86 #endif /* USE_DHCP */

87

88 /* When the netif link is down this function must be called.*/

89 netif_set_down(&gnetif);

90 }

91 }

ETH_link_callback函数被LwIP调用,当链路状态发送改变时该函数就被调用,用于状态改变后处理相关事务。首先调用netif_is_link_up函数判断新状态是否是链路启动状态,如果是启动状态就进入if语句,接下来会判断ETH是否被设置为自适应模式,如果不是自适应模式需要使用ETH_WritePHYRegister函数使能PHY工作为自适应模式,然后ETH_ReadPHYRegister函数读取PHY相关寄存器,获取PHY当前支持的以太网速度和工作模式,并保存到ETH_InitStructure结构体变量中。ETH_Start函数用于使能ETH外设,之后就是配置ETH的IP地址、子网掩码、网关,如果是定义了DHCP (动态主机配置协议)功能则启动DHCP。最后就是调用netif_set_up函数在LwIP层次配置启动ETH功能。

如果检测到是链路关闭状态,调用ETH_Stop函数关闭ETH,如果定义了DHCP功能则需关闭DHCP,最后调用netif_set_down函数在LwIP层次关闭ETH功能。

以上对文件修改部分更多涉及到ETH硬件底层驱动,一些是PHY芯片驱动函数、一些是ETH外设与LwIP连接函数。接下来要讲解的文件代码更多是与LwIP应用相关的。

netconf.c和netconf.h文件用于存放LwIP配置相关代码。netcon.h定义了相关宏。

代码清单 398 LwIP配置相关宏定义

1 /* DHCP状态 */

2 #define DHCP_START 1

3 #define DHCP_WAIT_ADDRESS 2

4 #define DHCP_ADDRESS_ASSIGNED 3

5 #define DHCP_TIMEOUT 4

6 #define DHCP_LINK_DOWN 5

7

8 //#define USE_DHCP /* enable DHCP, if disabled static address is used */

9

10 /* 调试信息输出 */

11 #define SERIAL_DEBUG

12 /* 远端IP地址和端口 */

13 #define DEST_IP_ADDR0 192

14 #define DEST_IP_ADDR1 168

15 #define DEST_IP_ADDR2 1

16 #define DEST_IP_ADDR3 105

17 #define DEST_PORT 6000

18

19 /* MAC地址:网卡地址 */

20 #define MAC_ADDR0 2

21 #define MAC_ADDR1 0

22 #define MAC_ADDR2 0

23 #define MAC_ADDR3 0

24 #define MAC_ADDR4 0

25 #define MAC_ADDR5 0

26

27 /*静态IP地址 */

28 #define IP_ADDR0 192

29 #define IP_ADDR1 168

30 #define IP_ADDR2 1

31 #define IP_ADDR3 122

32

33 /* 子网掩码 */

34 #define NETMASK_ADDR0 255

35 #define NETMASK_ADDR1 255

36 #define NETMASK_ADDR2 255

37 #define NETMASK_ADDR3 0

38

39 /* 网关 */

40 #define GW_ADDR0 192

41 #define GW_ADDR1 168

42 #define GW_ADDR2 1

43 #define GW_ADDR3 1

44

45 /* 检测PHY链路状态的实际间隔(单位:ms) */

46 #ifndef LINK_TIMER_INTERVAL

47 #define LINK_TIMER_INTERVAL 1000

48 #endif

49

50 /* MII and RMII mode selection ***********/

51 #define RMII_MODE

52 //#define MII_MODE

53

54 /* 在MII模式时,使能MCO引脚输出25MHz脉冲 */

55 #ifdef MII_MODE

56 #define PHY_CLOCK_MCO

57 #endif

USE_DHCP宏用于定义是否使用DHCP功能,如果不定义该宏,直接使用静态的IP地址,如果定义该宏,则使用DHCP功能,获取动态的IP地址,这里有个需要注意的地方,电脑是没办法提供DHCP服务功能的,路由器才有DHCP服务功能,使用当开发板直连电脑时不能定义该宏。

SERIAL_DEBUG宏是定义是否使能串口定义相关调试信息功能,一般选择使能,所以在main函数中需要添加串口初始化函数。

接下来,定义了远端IP和端口、MAC地址、静态IP地址、子网掩码、网关相关宏,可以根据实际情况修改。

LAN8720A仅支持RMII接口,根据硬件设计这里定义使用RMII_MODE。

代码清单 399 LwIP_Init函数

1 void LwIP_Init(void)

2 {

3 struct ip_addr ipaddr;

4 struct ip_addr netmask;

5 struct ip_addr gw;

6

7 /* Initializes the dynamic memory heap defined by MEM_SIZE.*/

8 mem_init();

9 /* Initializes the memory pools defined by MEMP_NUM_x.*/

10 memp_init();

11

12 #ifdef USE_DHCP

13 ipaddr.addr = 0;

14 netmask.addr = 0;

15 gw.addr = 0;

16 #else

17 IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);

18 IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1 ,

19 NETMASK_ADDR2,NETMASK_ADDR3);

20 IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);

21 #endif

22 /* 添加以太网设备 */

23 netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL,

24 &ethernetif_init, &ethernet_input);

25

26 /* 设置以太网设备为默认网卡 */

27 netif_set_default(&gnetif);

28

29 if (EthStatus == (ETH_INIT_FLAG | ETH_LINK_FLAG)) {

30 gnetif.flags |= NETIF_FLAG_LINK_UP;

31 /* 配置完成网卡后启动网卡*/

32 netif_set_up(&gnetif);

33 #ifdef USE_DHCP

34 DHCP_state = DHCP_START;

35 #else

36 #ifdef SERIAL_DEBUG

37 printf(“\n Static IP address \n”);

38 printf(“IP: %d.%d.%d.%d\n”,IP_ADDR0,IP_ADDR1,IP_ADDR2,IP_ADDR3);

39 printf(“NETMASK: %d.%d.%d.%d\n”,NETMASK_ADDR0,NETMASK_ADDR1,

40 NETMASK_ADDR2,NETMASK_ADDR3);

41 printf(“Gateway:%d.%d.%d.%d\n”,GW_ADDR0,GW_ADDR1,GW_ADDR2,GW_ADDR3);

42 #endif /* SERIAL_DEBUG */

43 #endif /* USE_DHCP */

44 } else {

45 /* 当网络链路关闭时关闭网卡设备 */

46 netif_set_down(&gnetif);

47 #ifdef USE_DHCP

48 DHCP_state = DHCP_LINK_DOWN;

49 #endif /* USE_DHCP */

50 #ifdef SERIAL_DEBUG

51 printf(“\n Network Cable is \n”);

52 printf(" not connected \n");

53 #endif /* SERIAL_DEBUG */

54 }

55 /* 设置链路回调函数,用于获取链路状态 */

56 netif_set_link_callback(&gnetif, ETH_link_callback);

57 }

LwIP_Init函数用于初始化LwIP协议栈,一般在main函数中调用。首先是内存相关初始化,mem_init函数是动态内存堆初始化,memp_init函数是存储池初始化,LwIP是实现内存的高效利用,内部需要不同形式的内存管理模式。

接下来为ipaddr、netmask和gw结构体变量赋值,设置本地IP地址、子网掩码和网关,如果使用DHCP功能直接赋值为0即可。netif_add是以太网设备添加函数,即向LwIP协议栈申请添加一个网卡设备,函数有7个形参,第一个为netif结构体类型变量指针,这里赋值为gnetif地址,该网卡设备属性就存放在gnetif变量中;第二个为ip_addr结构体类型变量指针,用于设置网卡IP地址;第三个ip_addr结构体类型变量指针,用于设置子网掩码;第四个为ip_addr结构体类型变量指针,用于设置网关;第五个为void变量,用户自定义字段,一般不用直接赋值NULL;第六个为netif_init_fn类型函数指针,用于指向网卡设备初始化函数,这里赋值为指向ethernetif_init函数,该函数在ethernetif.c文件定义,初始化LwIP与ETH外设连接函数;最后一个参数为netif_input_fn类型函数指针,用于指向以太网帧接收函数,这里赋值为指向ethernet_input函数,该函数定义在etharp.c文件中。

netif_set_default函数用于设置指定网卡为默认的网络通信设备。

在无硬件连接错误时,调用ETH_BSP_Config(优先LwIP_Init函数被调用)时会将EthStatus变量对应的ETH_LINK_FLAG位使能,所以在LwIP_INIT函数中会执行if判断语句代码,置位网卡设备标志位以及运行netif_set_up函数启动网卡设备。否则执行netif_set_down函数停止网卡设备。

最后,根据需要调用netif_set_link_callback函数实在当链路状态发生改变时需要调用的回调函数配置。

代码清单 3910 LwIP_Pkt_Handle函数

1 void LwIP_Pkt_Handle(void)

2 {

3 /* 从以太网存储器读取一个以太网帧并将其发送给LwIP */

4 ethernetif_input(&gnetif);

5 }

LwIP_Pkt_Handle函数用于从以太网存储器读取一个以太网帧并将其发送给LwIP,它在接收到以太网帧时被调用,它是直接调用ethernetif_input函数实现的,该函数定义在ethernetif.c文件中。

代码清单 3911 LwIP_Periodic_Handle函数

1 void LwIP_Periodic_Handle(__IO uint32_t localtime)

2 {

3 #if LWIP_TCP

4 /* TCP periodic process every 250 ms */

5 if (localtime - TCPTimer >= TCP_TMR_INTERVAL) {

6 TCPTimer = localtime;

7 tcp_tmr();

8 }

9 #endif

10

11 /* ARP periodic process every 5s */

12 if ((localtime - ARPTimer) >= ARP_TMR_INTERVAL) {

13 ARPTimer = localtime;

14 etharp_tmr();

15 }

16

17 /* Check link status periodically */

18 if ((localtime - LinkTimer) >= LINK_TIMER_INTERVAL) {

19 ETH_CheckLinkStatus(ETHERNET_PHY_ADDRESS);

20 LinkTimer=localtime;

21 }

22

23 #ifdef USE_DHCP

24 /* Fine DHCP periodic process every 500ms */

25 if (localtime - DHCPfineTimer >= DHCP_FINE_TIMER_MSECS) {

26 DHCPfineTimer = localtime;

27 dhcp_fine_tmr();

28 if ((DHCP_state != DHCP_ADDRESS_ASSIGNED) &&

29 (DHCP_state != DHCP_TIMEOUT) &&

30 (DHCP_state != DHCP_LINK_DOWN)) {

31 #ifdef SERIAL_DEBUG

32 LED1_TOGGLE;

33 printf(“\nFine DHCP periodic process every 500ms\n”);

34 #endif /* SERIAL_DEBUG */

35

36 /* process DHCP state machine */

37 LwIP_DHCP_Process_Handle();

38 }

39 }

40

41 /* DHCP Coarse periodic process every 60s */

42 if (localtime - DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS) {

43 DHCPcoarseTimer = localtime;

44 dhcp_coarse_tmr();

45 }

46

47 #endif

48 }

LwIP_Periodic_Handle函数是一个必须被无限循环调用的LwIP支持函数,一般在main函数的无限循环中调用,主要功能是为LwIP各个模块提供时间并查询链路状态,该函数有一个形参,用于指示当前时间,单位为ms。

对于TCP功能,每250ms执行一次tcp_tmr函数;对于ARP(地址解析协议),每5s执行一次etharp_tmr函数;对于链路状态检测,每1s执行一次ETH_CheckLinkStatus函数;对于DHCP功能,每500ms执行一次dhcp_fine_tmr函数,如果DHCP处于DHCP_START或DHCP_WAIT_ADDRESS状态就执行LwIP_DHCP_Process_Handle函数,对于DHCP功能,还有每60s执行一次dhcp_coarse_tmr函数。

代码清单 3912 LwIP_DHCP_Process_Handle函数

1 void LwIP_DHCP_Process_Handle(void)

2 {

3 struct ip_addr ipaddr;

4 struct ip_addr netmask;

5 struct ip_addr gw;

6

7 switch (DHCP_state) {

8 case DHCP_START: {

9 DHCP_state = DHCP_WAIT_ADDRESS;

10 dhcp_start(&gnetif);

11 /* IP address should be set to 0

12 every time we want to assign a new DHCP address */

13 IPaddress = 0;

14 #ifdef SERIAL_DEBUG

15 printf(“\n Looking for \n”);

16 printf(" DHCP server \n");

17 printf(" please wait… \n");

18 #endif /* SERIAL_DEBUG */

19 }

20 break;

21

22 case DHCP_WAIT_ADDRESS: {

23 /* Read the new IP address */

24 IPaddress = gnetif.ip_addr.addr;

25

26 if (IPaddress!=0) {

27 DHCP_state = DHCP_ADDRESS_ASSIGNED;

28 /* Stop DHCP */

29 dhcp_stop(&gnetif);

30 #ifdef SERIAL_DEBUG

31 printf(“\n IP address assigned \n”);

32 printf(" by a DHCP server \n");

33 printf(“IP: %d.%d.%d.%d\n”,(uint8_t)(IPaddress),

34 (uint8_t)(IPaddress >> 8),(uint8_t)(IPaddress >> 16),

35 (uint8_t)(IPaddress >> 24));

36 printf(“NETMASK: %d.%d.%d.%d\n”,NETMASK_ADDR0,NETMASK_ADDR1,

37 NETMASK_ADDR2,NETMASK_ADDR3);

38 printf(“Gateway: %d.%d.%d.%d\n”,GW_ADDR0,GW_ADDR1,

39 GW_ADDR2,GW_ADDR3);

40 LED1_ON;

41 #endif /* SERIAL_DEBUG */

42 } else {

43 /* DHCP timeout */

44 if (gnetif.dhcp->tries > MAX_DHCP_TRIES) {

45 DHCP_state = DHCP_TIMEOUT;

46 /* Stop DHCP */

47 dhcp_stop(&gnetif);

48 /* Static address used */

49 IP4_ADDR(&ipaddr, IP_ADDR0 ,IP_ADDR1 , IP_ADDR2 , IP_ADDR3 );

50 IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1,

51 NETMASK_ADDR2, NETMASK_ADDR3);

52 IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);

53 netif_set_addr(&gnetif, &ipaddr , &netmask, &gw);

54 #ifdef SERIAL_DEBUG

55 printf(“\n DHCP timeout \n”);

56 printf(" Static IP address \n");

57 printf(“IP: %d.%d.%d.%d\n”,IP_ADDR0,IP_ADDR1,

58 IP_ADDR2,IP_ADDR3);

59 printf(“NETMASK: %d.%d.%d.%d\n”,NETMASK_ADDR0,NETMASK_ADDR1,

60 NETMASK_ADDR2,NETMASK_ADDR3);

61 printf(“Gateway: %d.%d.%d.%d\n”,GW_ADDR0,GW_ADDR1,

62 GW_ADDR2,GW_ADDR3);

63 LED1_ON;

64 #endif /* SERIAL_DEBUG */

65 }

66 }

67 }

68 break;

69 default:

70 break;

71 }

72 }

LwIP_DHCP_Process_Handle函数用于执行DHCP功能,当DHCP状态为DHCP_START时,执行dhcp_start函数启动DHCP功能,LwIP会向DHCP服务器申请分配IP请求,并进入等待分配状态。当DHCP状态为DHCP_WAIT_ADDRESS时,先判断IP地址是否为0,如果不为0说明已经有IP地址,DHCP功能已经完成可以停止它;如果IP地址总是为0,就需要判断是否超过最大等待时间,并提示出错。

lwipopts.h文件存放一些宏定义,用于剪切LwIP功能,比如有无操作系统、内存空间分配、存储池分配、TCP功能、DHCP功能、UDP功能选择等等。这里使用与ST官方例程相同配置即可。

LwIP为使用者提供了两种应用程序接口(API函数)来实现TCP/IP协议栈,一种是低水平、基于回调函数的API,称为RAW API,另外一种是高水平、连续的API,称为sequential API,sequential API又有两种函数结构,一种是Netconn,一种是Socket,它与在电脑端使用的BSD标准的Socket API结构和原理是非常相似的。

接下来内容我们使用RAW API实现一个简单的TCP通信测试,ST官方有提供相关的例程,我们对其内容稍作调整。代码内容存放在tcp_echoclient.c文件中。TCP在各个层次处理过程见图 3919。

图 3919 TCP处理过程

网络接口层的netif->output和netif->input是在ethernetif.c文件中实现的,网络层和传输层有LwIP协议栈实现,应用层代码就是用户使用LwIP函数实现网络功能。

代码清单 3913 tcp_echoclient_connect函数

1 void tcp_echoclient_connect(void)

2 {

3 struct ip_addr DestIPaddr;

4

5 /* create new tcp pcb */

6 echoclient_pcb = tcp_new();

7

8 if (echoclient_pcb != NULL) {

9 IP4_ADDR( &DestIPaddr, DEST_IP_ADDR0, DEST_IP_ADDR1,

10 DEST_IP_ADDR2, DEST_IP_ADDR3 );

11

12 /* connect to destination address/port */

13 tcp_connect(echoclient_pcb,&DestIPaddr,

14 DEST_PORT,tcp_echoclient_connected);

15 } else {

16 /* deallocate the pcb */

17 memp_free(MEMP_TCP_PCB, echoclient_pcb);

18 #ifdef SERIAL_DEBUG

19 printf(“\n\r can not create tcp pcb”);

20 #endif

21 }

22 }

tcp_echoclient_connect函数用于创建TCP从设备并启动与TCP服务器连接。tcp_new函数创建一个新TCP协议控制块,主要是必要的内存申请,返回一个未初始化的TCP协议控制块指针。如果返回值不了0就可以使用tcp_connect函数连接到TCP服务器,tcp_connect函数用于TCP从设备连接至指定IP地址和端口的TCP服务器,它有四个形参,第一个为TCP协议控制块指针,第二个为服务器IP地址,第三个为服务器端口,第四个为函数指针,当连接正常建立时或连接错误时函数被调用,这里赋值tcp_echoclient_connected函数名。如果tcp_new返回值为0说明创建TCP协议控制块失败,调用memp_free函数释放相关内容。

代码清单 3914 tcp_echoclient_disconnect函数

1 struct echoclient {

2 enum echoclient_states state; /* connection status */

3 struct tcp_pcb *pcb; /* pointer on the current tcp_pcb */

4 struct pbuf *p_tx; /* pointer on pbuf to be transmitted */

5 };

6

7 void tcp_echoclient_disconnect(void)

8 {

9 /* close connection */

10 tcp_echoclient_connection_close(echoclient_pcb,echoclient_es);

11 #ifdef SERIAL_DEBUG

12 printf(“\n\r close TCP connection”);

13 #endif

14 }

echoclient是自定义的一个结构体类型,包含了TCP从设备的状态、TCP协议控制块指针和发送数据指针。tcp_echoclient_disconnect函数用于断开TCP连接,通过调用tcp_echoclient_connection_close函数实现,它有两个形参,一个是TCP协议控制块,一个是echoclient类型指针。

代码清单 3915 tcp_echoclient_connected函数

1 static err_t tcp_echoclient_connected(void *arg, struct tcp_pcb *tpcb,

2 err_t err)

3 {

4 struct echoclient *es = NULL;

5

6 if (err == ERR_OK) {

7 /* allocate structure es to maintain tcp connection informations */

8 es = (struct echoclient *)mem_malloc(sizeof(struct echoclient));

9 echoclient_es=es;

10 if (es != NULL) {

11 es->state = ES_CONNECTED;

12 es->pcb = tpcb;

13 sprintf((char*)data, “sending tcp client message %d”,

14 message_count);

15 /* allocate pbuf */

16 es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data),

17 PBUF_POOL);

18 if (es->p_tx) {

19 /* copy data to pbuf */

20 pbuf_take(es->p_tx, (char*)data, strlen((char*)data));

21 /* pass newly allocated es structure as argument to tpcb */

22 tcp_arg(tpcb, es);

23 /* initialize LwIP tcp_recv callback function */

24 tcp_recv(tpcb, tcp_echoclient_recv);

25 /* initialize LwIP tcp_sent callback function */

26 tcp_sent(tpcb, tcp_echoclient_sent);

27 /* initialize LwIP tcp_poll callback function */

28 tcp_poll(tpcb, tcp_echoclient_poll, 1);

29 /* send data */

30 tcp_echoclient_send(tpcb,es);

31 return ERR_OK;

32 }

33 } else {

34 /* close connection */

35 tcp_echoclient_connection_close(tpcb, es);

36 /* return memory allocation error */

37 return ERR_MEM;

38 }

39 } else {

40 /* close connection */

41 tcp_echoclient_connection_close(tpcb, es);

42 }

43 return err;

44 }

tcp_echoclient_connected函数作为tcp_connect函数设置的回调函数,在TCP建立连接时被调用,这里实现的功能是向TCP服务器发送一段数据。使用mem_malloc函数申请内存空间存放echoclient结构体类型数据,并赋值给es指针变量。如果内存申请失败调用tcp_echoclient_connection_close函数关闭TCP连接;确保内存申请成功后为es成员赋值,p_tx成员是发送数据指针,这里使用pbuf_alloc函数向内存池申请存放发送数据的存储空间,即数据发送缓冲区。确保发送数据存储空间申请成功后使用pbuf_take函数将待发送数据data拷贝到数据发送存储器。tcp_arg函数用于设置用户自定义参数,使得该参数可在相关回调函数被重新使用。tcp_recv、tcp_sent和tcp_poll函数分别设置TCP协议控制块对应的接收、发送和轮询回调函数。最后调用tcp_echoclient_send函数发送数据。

代码清单 3916 tcp_echoclient_recv函数

1 static err_t tcp_echoclient_recv(void *arg, struct tcp_pcb *tpcb,

2 struct pbuf *p, err_t err)

3 {

4 char *recdata=0;

5 struct echoclient *es;

6 err_t ret_err;

7

8 LWIP_ASSERT(“arg != NULL”,arg != NULL);

9 es = (struct echoclient *)arg;

10 /* if we receive an empty tcp frame from server => close connection */

11 if (p == NULL) {

12 /* remote host closed connection */

13 es->state = ES_CLOSING;

14 if (es->p_tx == NULL) {

15 /* we’re done sending, close connection */

16 tcp_echoclient_connection_close(tpcb, es);

17 } else {

18 /* send remaining data*/

19 tcp_echoclient_send(tpcb, es);

20 }

21 ret_err = ERR_OK;

22 }

23 /* else : a non empty frame was received from echo server

24 but for some reason err != ERR_OK */

25 else if (err != ERR_OK) {

26 /* free received pbuf*/

27 pbuf_free§;

28 ret_err = err;

29 } else if (es->state == ES_CONNECTED) {

30 /* increment message count */

31 message_count++;

32 /* Acknowledge data reception */

33 tcp_recved(tpcb, p->tot_len);

34 #ifdef SERIAL_DEBUG

35 recdata=(char *)malloc(p->len*sizeof(char));

36 if (recdata!=NULL) {

37 memcpy(recdata,p->payload,p->len);

38 printf(“upd_rec<<%s”,recdata);

39 }

40 free(recdata);

41 #endif

42 /* free received pbuf*/

43 pbuf_free§;

44 ret_err = ERR_OK;

45 }

46 /* data received when connection already closed */

47 else {

48 /* Acknowledge data reception */

49 tcp_recved(tpcb, p->tot_len);

50

51 /* free pbuf and do nothing */

52 pbuf_free§;

53 ret_err = ERR_OK;

54 }

55 return ret_err;

56 }

tcp_echoclient_recv函数是TCP接收回调函数,TCP从设备接收到数据时该函数就被运行一次,我们可以提取数据帧内容。函数先检测是否为空帧,如果为空帧则关闭TCP连接,然后检测是否发生传输错误,如果发送错误执行pbuf_free函数释放内存。检查无错误就可以调用tcp_recved函数接收数据,这样就可以提取接收到信息。最后调用pbuf_free函数释放相关内存。

代码清单 3917 tcp_echoclient_send函数

1 static void tcp_echoclient_send(struct tcp_pcb *tpcb, struct echoclient * es)

2 {

3 struct pbuf *ptr;

4 err_t wr_err = ERR_OK;

5

6 while ((wr_err == ERR_OK) &&

7 (es->p_tx != NULL) &&

8 (es->p_tx->len <= tcp_sndbuf(tpcb))) {

9

10 /* get pointer on pbuf from es structure */

11 ptr = es->p_tx;

12

13 /* enqueue data for transmission */

14 wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);

15

16 if (wr_err == ERR_OK) {

17 /* continue with next pbuf in chain (if any) */

18 es->p_tx = ptr->next;

19

20 if (es->p_tx != NULL) {

21 /* increment reference count for es->p */

22 pbuf_ref(es->p_tx);

23 }

24

25 /* free pbuf: will free pbufs up to es->p

26 (because es->p has a reference count > 0) */

27 pbuf_free(ptr);

28 } else if (wr_err == ERR_MEM) {

29 /* we are low on memory, try later, defer to poll */

30 es->p_tx = ptr;

31 } else {

32 /* other problem ?? */

33 }

34 }

35 }

tcp_echoclient_send函数用于TCP数据发送,它有两个形参,一个是TCP协议控制块结构体指针,一个是echoclient结构体指针。在判断待发送数据存在并不超过最大可用发送队列数据数后,执行tcp_write函数将待发送数据写入发送队列,由协议内核决定发送时机。

代码清单 3918 tcp_echoclient_poll函数

1 static err_t tcp_echoclient_poll(void *arg, struct tcp_pcb *tpcb)

2 {

3 err_t ret_err;

4 struct echoclient *es;

5

6 es = (struct echoclient*)arg;

7 if (es != NULL) {

8 if (es->p_tx != NULL) {

9 /* there is a remaining pbuf (chain) , try to send data */

10 tcp_echoclient_send(tpcb, es);

11 } else {

12 /* no remaining pbuf (chain) */

13 if (es->state == ES_CLOSING) {

14 /* close tcp connection */

15 tcp_echoclient_connection_close(tpcb, es);

16 }

17 }

18 ret_err = ERR_OK;

19 } else {

20 /* nothing to be done */

21 tcp_abort(tpcb);

22 ret_err = ERR_ABRT;

23 }

24 return ret_err;

25 }

tcp_echoclient_poll函数是由tcp_poll函数指定的回调函数,它每500ms执行一次,函数检测是否有待发送数据,如果有就执行tcp_echoclient_send函数发送数据。

代码清单 3919 tcp_echoclient_sent函数

1 static err_t tcp_echoclient_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)

2 {

3 struct echoclient *es;

4

5 LWIP_UNUSED_ARG(len);

6

7 es = (struct echoclient *)arg;

8

9 if (es->p_tx != NULL) {

10 /* still got pbufs to send */

11 tcp_echoclient_send(tpcb, es);

12 }

13

14 return ERR_OK;

15 }

tcp_echoclient_sent函数是有tcp_sent函数指定的回调函数,当接收到远端设备发送应答信号时被调用,它实际是通过调用tcp_echoclient_send函数发送数据实现的。

代码清单 3920 tcp_echoclient_connection_close函数

1 static void tcp_echoclient_connection_close(struct tcp_pcb *tpcb,

2 struct echoclient * es )

3 {

4 /* remove callbacks */

5 tcp_recv(tpcb, NULL);

6 tcp_sent(tpcb, NULL);

7 tcp_poll(tpcb, NULL,0);

8

9 if (es != NULL) {

10 mem_free(es);

11 }

12 /* close tcp connection */

13 tcp_close(tpcb);

14 }

tcp_echoclient_connection_close函数用于关闭TCP连接,将相关的回调函数解除,释放es变量内存,最后调用tcp_close函数关闭TCP连接,释放TCP协议控制块内存。

代码清单 3921 定时器初始化配置及中断服务函数

1 /* 初始化配置TIM3,使能每10ms发生一次中断 */

2 static void TIM3_Config(uint16_t period,uint16_t prescaler)

3 {

4 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

5 NVIC_InitTypeDef NVIC_InitStructure;

6

7 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能TIM3时钟

8

9 TIM_TimeBaseInitStructure.TIM_Prescaler=prescaler; //定时器分频

10 TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;

11 TIM_TimeBaseInitStructure.TIM_Period=period; //自动重装载值

12 TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;

13

14 TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);

15

16 TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断

17 TIM_Cmd(TIM3,ENABLE); //使能定时器3

18

19 NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断

20 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01;

21 NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03;

22 NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;

23 NVIC_Init(&NVIC_InitStructure);

24 }

25

26 /* TIM3中断服务函数 */

27 void TIM3_IRQHandler(void)

28 {

29 if (TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) { //溢出中断

30 LocalTime+=10;//10ms增量

31 }

32 TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位

33 }

LwIP_Periodic_Handle函数执行LwIP需要周期性执行函数,该所以我们需要为该函数提高一个时间基准,这里使用TIM3产生这个基准,初始化配置TIM3每10ms中断一次,在其中断服务函数中递增LocalTime变量值。

代码清单 3922 main函数

1 int main(void)

2 {

3 uint8_t flag=0;

4 /* 初始化LED */

5 LED_GPIO_Config();

6

7 /* 初始化按键 */

8 Key_GPIO_Config();

9

10 /* 初始化调试串口,一般为串口1 */

11 Debug_USART_Config();

12

13 /* 初始化系统滴答定时器 */

14 SysTick_Init();

15

16 TIM3_Config(999,899);//10ms定时器

17 printf(“以太网通信实现例程\n”);

18

19 /* Configure ethernet (GPIOs, clocks, MAC, DMA) */

20 ETH_BSP_Config();

21 printf(“PHY初始化结束\n”);

22

23 /* Initilaize the LwIP stack */

24 LwIP_Init();

25

26 printf(" KEY1: 启动TCP连接\n");

27 printf(" KEY2: 断开TCP连接\n");

28

29 /* IP地址和端口可在netconf.h文件修改,或者使用DHCP服务自动获取IP

30 (需要路由器支持)*/

31 printf(“本地IP和端口: %d.%d.%d.%d\n”,IP_ADDR0,IP_ADDR1,

32 IP_ADDR2,IP_ADDR3);

33 printf(“远端IP和端口: %d.%d.%d.%d:%d\n”,DEST_IP_ADDR0, DEST_IP_ADDR1,

34 DEST_IP_ADDR2, DEST_IP_ADDR3,DEST_PORT);

35

36 while (1) {

37 if ((Key_Scan(KEY1_GPIO_PORT,KEY1_PIN)KEY_ON) && (flag0)) {

38 LED2_ON;

39 if (EthLinkStatus == 0) {

40 printf(“connect to tcp server\n”);

41 /*connect to tcp server */

42 tcp_echoclient_connect();

43 flag=1;

44 }

45 }

46 if ((Key_Scan(KEY2_GPIO_PORT,KEY2_PIN)==KEY_ON) && flag) {

47 LED2_OFF;

48 tcp_echoclient_disconnect();

49 flag=0;

50 }

51 /* check if any packet received */

52 if (ETH_CheckFrameReceived()) {

53 /* process received ethernet packet */

54 LwIP_Pkt_Handle();

55 }

56 /* handle periodic timers for LwIP */

57 LwIP_Periodic_Handle(LocalTime);

58 }

59 }

首先是初始化LED指示灯、按键、调试串口、系统滴答定时器,TIM3_Config函数配置10ms定时并启动定时器,ETH_BSP_Config函数初始化ETH相关GPIO、配置MAC和DMA并获取PHY状态,LwIP_Init函数初始化LwIP协议栈。进入无限循环函数,不断检测按键状态,如果KEY1被按下则调用tcp_echoclient_connect函数启动TCP连接,如果KEY2被按下则调用tcp_echoclient_disconnect关闭TCP连接。ETH_CheckFrameReceived函数用于检测是否接收到数据帧,如果接收到数据帧则调用LwIP_Pkt_Handle函数将数据帧从缓冲区传入LwIP。LwIP_Periodic_Handle函执行必须被周期调用的函数。

下载验证

保证开发板相关硬件连接正确,用USB线连接开发板"USB TO UART"接口跟电脑,在电脑端打开串口调试助手并配置好相关参数;使用网线连接开发板网口跟路由器,这里要求电脑连接在同一个路由器上,之所以使用路由器是这样连接方便,电脑端无需更多操作步骤,并且路由器可以提供DHCP服务器功能,而电脑不行的,最后在电脑端打开网络调试助手软件,并设置相关参数,见图 3920,调试助手的设置与netconf.h文件中相关宏定义是对应的,不同电脑设置情况可能不同。把编译好的程序下载到开发板。

图 3920 调试助手设置界面

在系统硬件初始化时串口调试助手会打印相关提示信息,等待初始化完成后可打开电脑端CMD窗口,输入ping命令测试开发板链路,图 3921为链路正常情况,如果出现ping不同情况,检查网线连接。

图 3921 ping窗口

ping状态正常后,可按下开发板KEY1按键,使能开发板连接电脑端的TCP服务器,之后就可以进行数据传输,需要接收传输时可以按下开发板KEY2按键,实际操作调试助手界面见图 3922。

图 3922 调试助手接发通信效果

39.9 基于uCOS-III移植LwIP实验

上面的实验是无操作系统移植LwIP,LwIP也确实是支持无操作系统移植运行,这对于芯片资源紧张、不合适运行操作系统环境还是有很大用处的。不过在很多应用中会采用操作系统上运行LwIP,这有利于提高整体性能。这个实验我们主要讲解移植操作步骤,过程中直接使用上个实验LwIP底层驱动,除非有需要修改地方才指出,同时这里假设已有移植好的uCOS-III工程可参考使用,关于uCOS-III移植部分可参考我们相关文档,这里主要介绍LwIP使用uCOS-III信号量、消息队列、定时器函数等等函数接口。

这个实验最终实现在uCOS-III操作系统基础上移植LwIP,使能DHCP功能,在动态获取IP之后即可ping通。运行uCOS-III操作系统之后一般会使用Netconn或Socket方法使用LwIP,关于这两个的应用方法限于篇幅问题这里不做深入探究。

UCOS-III和LwIP都是属于软件编程层次,所以硬件设计部分并不需要做更改,直接使用上个实验的硬件设计即可。

接下来开始介绍移植步骤,为简化移植步骤,我们的思路是直接使用uCOS-III例程,在其基础上移植LwIP部分。

第一步:文件拷贝

拷贝整个uCOS-III工程,修改文件夹名称为"ETH—基于uCOS-III的LwIP移植",作为我们这个实验工程基础,我们在此基础上添加功能。拷贝上个实验工程中的lwip-1.4.1整个文件夹到USER文件夹(路径:…\ETH—基于uCOS-III的LwIP移植\USER)中。

LwIP源码部分,即src文件夹,内容是不用修改的,只有port文件夹内容需要修改。在stsw-stm32070文件夹找到FreeRTOS文件夹(路径:… \Utilities\Third_Party\lwip-1.4.1\port \STM32F4x7\FreeRTOS),该文件夹内容是LwIP与FreeRTOS操作系统连接的相关接口函数,虽然我们选择使用uCOS-III操作系统,当还是有很多可以借鉴的地方,移植过程我们采用修改这些文件方法实现而不是完全自己新建文件,把FreeRTOS整个文件夹拷贝到port文件夹(路径:…\ETH—基于uCOS-III的LwIP移植\USER\lwip-1.4.1\port)内,并改名为UCOS305,此时port文件夹内有三个文件夹,分别为:arch、Standalone、UCOS305,其中Standalone在本实验是不被使用的。

把上个实验工程中的App文件夹拷贝到本实验相同位置,其中tcp_echoclient.c和tcp_echoclient.h文件不是本实验需要的,将其删除。netconf.c、netconf.h和lwipopts.h三个文件是必需的,但因为如果在本实验直接使用lwipopts.h文件需要修改较多地方,我们先将该文件删除,然后在stsw-stm32070文件夹找到httpserver_socket文件夹。(路径:… \Utilities\Third_Party\lwip-1.4.1\port \STM32F4x7\FreeRTOS\httpserver_socket),在该文件夹下inc文件夹中的lwipopts.h文件是更方便我们移植的文件,我们拷贝它到App文件夹中。

最后,把上个实验工程中的ETH文件夹拷贝到本实验相同位置,这个文件夹内容都是必需的,但我们不用进行修改。

第二步:为工程添加文件

与上个工程相比,LwIP部分文件只有port文件夹文件有所修改,其他使用与上个实验相同文件结构皆可,最终工程文件结构参考图 3923。

图 3923 工程文件结构

添加完源文件后,还需要在工程选项中设置添加头文件路径,参考图 3924。

图 3924 添加头文件路径

第三步:文件修改

ETH文件夹内文件,stm32f429_eth.c、stm32f429_eth.h、stm32f429_phy.c和stm32f429_phy.h四个文件是ETH外部和PHY相关驱动,本实验并无需修改硬件,所以这四个文件内容不用修改,stm32f429_eth_conf.h文件是与ETH外设相关硬件宏定义,因为本实验使用操作系统,对延时函数定义与上个实验工程有所不同,需要稍作修改。

代码清单 3923 延时函数定义

1 #ifdef USE_Delay

2 #include “Bsp/bsp.h”

3 #define _eth_delay_ Delay_10ms

4 #else

5 #define _eth_delay_

6 #endif

这里使用在bsp.h文件中定义的Delay_10ms延时函数。

sys_arch.h和sys_arch.c两个文件是LwIP与uCOS-III连接的实现代码。sys_arch.h存放相关宏定义和类型定义。

代码清单 3924 宏定义

1 #define LWIP_STK_SIZE 512

2 #define LWIP_TASK_MAX 8

3

4 #define LWIP_TSK_PRIO 3

5 #define LWIP_TASK_START_PRIO LWIP_TSK_PRIO

6 #define LWIP_TASK_END_PRIO LWIP_TSK_PRIO +LWIP_TASK_MAX

7

8 #define MAX_QUEUES 10 // 消息邮箱的数量

9 #define MAX_QUEUE_ENTRIES 20 // 每个邮箱的大小

10

11 #define SYS_MBOX_NULL (void *)0

12 #define SYS_SEM_NULL (void *)0

13

14 #define sys_arch_mbox_tryfetch(mbox,msg) sys_arch_mbox_fetch(mbox,msg,1)

宏LWIP_STK_SIZE定义LwIP任务栈空间大小,实际空间是4*LWIP_STK_SIZE个字节。宏LWIP_TASK_MAX定义预留给LwIP使用的最大任务数量。LWIP_TSK_PRIO、LWIP_TASK_START_PRIO和LWIP_TASK_END_PRIO三个宏指定LwIP任务的优先级范围。宏MAX_QUEUES定义LwIP可以使用的最大邮箱数量,宏MAX_QUEUE_ENTRIES定义每个邮箱的大小。宏SYS_MBOX_NULL和SYS_SEM_NULL分别定义邮箱和信号量NULL对于的值。sys_arch_mbox_tryfetch函数是尝试获取邮箱内容,这里直接调用sys_arch_mbox_fetch函数实现。

代码清单 3925 类型定义

1 typedef OS_SEM sys_sem_t; // type of semiphores

2 typedef OS_MUTEX sys_mutex_t; // type of mutex

3 typedef OS_Q sys_mbox_t; // type of mailboxes

4 typedef CPU_INT08U sys_thread_t; // type of id of the new thread

5

6 typedef CPU_INT08U sys_prot_t;

不同操作系统有不同名称定义信号量、复合信号、邮箱、任务ID等等,这里使用uCOS-III操作系统需要使用对应的名称。

实际上,除了需要定于与操作系统对应的名称之外,还需要定于与编译器相关的名称,在sys_arch.h文件中有引用了cc.h头文件,因为我们使用Windows操作系统的Keil开发工具,需要对cc.h文件进行必须修改。

代码清单 3926 编译器相关类型定于和宏定义

1 typedef u32_t mem_ptr_t;

2 //typedef int sys_prot_t;

3

4

5 //#define U16_F “hu”

6 //#define S16_F “d”

7 //#define X16_F “hx”

8 //#define U32_F “u”

9 //#define S32_F “d”

10 //#define X32_F “x”

11 //#define SZT_F “uz”

12

13 #define U16_F “4d”

14 #define S16_F “4d”

15 #define X16_F “4x”

16 #define U32_F “8ld”

17 #define S32_F “8ld”

18 #define X32_F “8lx”

sys_prot_t类型已在sys_arch.h文件中定于,在cc.h文件必须注释掉不被使用。U16_F、S16_F、X16_F等等一系列名称用于LwIP的调试函数,这一系列宏定于用于调试信息输出格式化。

代码清单 3927 调试信息输出定于

1 #define LWIP_PLATFORM_DIAG(x) {printf x;}

2

3 #define LWIP_PLATFORM_ASSERT(x) do { printf("Assertion “%s” failed at \

4 line %d in %s\n",x, __LINE__, __FILE__);} while(0)

5

6 #define LWIP_ERROR(message, expression, handler) do { if (!(expression)) { \

7 printf(“Assertion “%s” failed at line %d in %s\n”, message, \

8 __LINE__, __FILE__); fflush(NULL);handler;} } while(0)

LwIP实现代码已经添加了调试信息功能,我们只需要定于信息输出途径即可,这里直接使用printf函数,将调试信息打印到串口调试助手。

sys_arch.c文件存放uCOS-III与LwIP连接函数,LwIP为实现在操作系统上运行,预留了相关接口函数,不同操作系统使用不同方法实现要求的功能。该文件存放在UCOS305文件夹内。

代码清单 3928 sys_now函数

1 u32_t sys_now()

2 {

3 OS_TICK os_tick_ctr;

4 CPU_SR_ALLOC();

5

6 CPU_CRITICAL_ENTER();

7 os_tick_ctr = OSTickCtr;

8 CPU_CRITICAL_EXIT();

9

10 return os_tick_ctr;

11 }

sys_now函数用于为LwIP提供系统时钟,这里直接的读取OSTickCtr变量值。CPU_CRITICAL_ENTER和CPU_CRITICAL_EXIT分别是关闭总中断和开启总中断。

LwIP的邮箱用于缓存和传递数据包。

代码清单 3929 邮箱创建与删除

1 err_t sys_mbox_new(sys_mbox_t *mbox, int size)

2 {

3 OS_ERR ucErr;

4

5 OSQCreate(mbox,“LWIP quiue”, size, &ucErr);

6 LWIP_ASSERT( "OSQCreate ", ucErr == OS_ERR_NONE );

7

8 if ( ucErr == OS_ERR_NONE) {

9 return 0;

10 }

11 return -1;

12 }

13

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

tryfetch(mbox,msg) sys_arch_mbox_fetch(mbox,msg,1)

宏LWIP_STK_SIZE定义LwIP任务栈空间大小,实际空间是4*LWIP_STK_SIZE个字节。宏LWIP_TASK_MAX定义预留给LwIP使用的最大任务数量。LWIP_TSK_PRIO、LWIP_TASK_START_PRIO和LWIP_TASK_END_PRIO三个宏指定LwIP任务的优先级范围。宏MAX_QUEUES定义LwIP可以使用的最大邮箱数量,宏MAX_QUEUE_ENTRIES定义每个邮箱的大小。宏SYS_MBOX_NULL和SYS_SEM_NULL分别定义邮箱和信号量NULL对于的值。sys_arch_mbox_tryfetch函数是尝试获取邮箱内容,这里直接调用sys_arch_mbox_fetch函数实现。

代码清单 3925 类型定义

1 typedef OS_SEM sys_sem_t; // type of semiphores

2 typedef OS_MUTEX sys_mutex_t; // type of mutex

3 typedef OS_Q sys_mbox_t; // type of mailboxes

4 typedef CPU_INT08U sys_thread_t; // type of id of the new thread

5

6 typedef CPU_INT08U sys_prot_t;

不同操作系统有不同名称定义信号量、复合信号、邮箱、任务ID等等,这里使用uCOS-III操作系统需要使用对应的名称。

实际上,除了需要定于与操作系统对应的名称之外,还需要定于与编译器相关的名称,在sys_arch.h文件中有引用了cc.h头文件,因为我们使用Windows操作系统的Keil开发工具,需要对cc.h文件进行必须修改。

代码清单 3926 编译器相关类型定于和宏定义

1 typedef u32_t mem_ptr_t;

2 //typedef int sys_prot_t;

3

4

5 //#define U16_F “hu”

6 //#define S16_F “d”

7 //#define X16_F “hx”

8 //#define U32_F “u”

9 //#define S32_F “d”

10 //#define X32_F “x”

11 //#define SZT_F “uz”

12

13 #define U16_F “4d”

14 #define S16_F “4d”

15 #define X16_F “4x”

16 #define U32_F “8ld”

17 #define S32_F “8ld”

18 #define X32_F “8lx”

sys_prot_t类型已在sys_arch.h文件中定于,在cc.h文件必须注释掉不被使用。U16_F、S16_F、X16_F等等一系列名称用于LwIP的调试函数,这一系列宏定于用于调试信息输出格式化。

代码清单 3927 调试信息输出定于

1 #define LWIP_PLATFORM_DIAG(x) {printf x;}

2

3 #define LWIP_PLATFORM_ASSERT(x) do { printf("Assertion “%s” failed at \

4 line %d in %s\n",x, __LINE__, __FILE__);} while(0)

5

6 #define LWIP_ERROR(message, expression, handler) do { if (!(expression)) { \

7 printf(“Assertion “%s” failed at line %d in %s\n”, message, \

8 __LINE__, __FILE__); fflush(NULL);handler;} } while(0)

LwIP实现代码已经添加了调试信息功能,我们只需要定于信息输出途径即可,这里直接使用printf函数,将调试信息打印到串口调试助手。

sys_arch.c文件存放uCOS-III与LwIP连接函数,LwIP为实现在操作系统上运行,预留了相关接口函数,不同操作系统使用不同方法实现要求的功能。该文件存放在UCOS305文件夹内。

代码清单 3928 sys_now函数

1 u32_t sys_now()

2 {

3 OS_TICK os_tick_ctr;

4 CPU_SR_ALLOC();

5

6 CPU_CRITICAL_ENTER();

7 os_tick_ctr = OSTickCtr;

8 CPU_CRITICAL_EXIT();

9

10 return os_tick_ctr;

11 }

sys_now函数用于为LwIP提供系统时钟,这里直接的读取OSTickCtr变量值。CPU_CRITICAL_ENTER和CPU_CRITICAL_EXIT分别是关闭总中断和开启总中断。

LwIP的邮箱用于缓存和传递数据包。

代码清单 3929 邮箱创建与删除

1 err_t sys_mbox_new(sys_mbox_t *mbox, int size)

2 {

3 OS_ERR ucErr;

4

5 OSQCreate(mbox,“LWIP quiue”, size, &ucErr);

6 LWIP_ASSERT( "OSQCreate ", ucErr == OS_ERR_NONE );

7

8 if ( ucErr == OS_ERR_NONE) {

9 return 0;

10 }

11 return -1;

12 }

13

[外链图片转存中…(img-08j3WVPv-1715623866211)]
[外链图片转存中…(img-skCwN8zE-1715623866212)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值