收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事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(Ð_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(Ð_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 ðernetif_init, ðernet_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
既有适合小白学习的零基础资料,也有适合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%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新