前言

  marvell88w8801这个代码在一年当中已经被我断断续续重构了无数次,我也不知道我为什么要这么做,可能只是觉得画了钱买了这个玩意弄不明白有点亏吧哈哈(不过最后还是不算弄明白QAQ)。不过之所以做个介绍,还是要给坑的bro做点提醒。

代码架构

整个wifi模块的代码架构如下所示
在这里插入图片描述

  • spi_drv.c: 最终数据是通过spi传输给marvell88w8801模块,然后wifi模块把数据发送数据
  • wifi_sd_drv.c: 对于WIFI模块本身协议用的SDIO协议,而SDIO协议是支持SPI模式的(因为SDIO引脚我内块板子没引出来,所以要使用SPI模式模拟SDIO的CMD发送)
  • wifi_transfer.c: 因为这个WIFI模块传输主要依赖CMD52/53指令,所以要做的就是依据数据手册把传输的data封装到这两条指令中,供上层调用
  • wifi_fm.c: 这个模块很离谱 每次上电都得先把固件加载到内存中 它自己没法保存(也可能是我没注意)
  • wifi_drv.c: 依据数据手册对WIFI模块常用的指令进行封装,比如WIFI模块的握手,开启AP热点等等。以及对于接收到的数据包进行解析,封装数据帧等
  • wifi.c: 上层驱动实现,和LWIP协议栈交互,实际上就是数据链路层的驱动(类比于以太网驱动)

SPI驱动子系统–参考Zephyr

  本质是为了装逼,不过这样不就可以说什么了解了Zephyr的SPI驱动架构实现了吗哈哈。Zephyr的SPI驱动提供了同步和异步两种方案,在这里都复刻了一部分过来了.(真正的实现还是非常复杂的,请参考:https://docs.zephyrproject.org/latest/hardware/peripherals/spi.html)
  不过这个模块似乎顶不住太高的SPI速率,一旦时钟分频系数过高就无法正确接收数据了。(也可能因为我是拿杜邦线连接导致的)

  • 面向对象的设计:spi-controller封装
    核心就是分配/设置/注册一个spi_controller结构体,具体的SPI外设对应的nunber由宏定义
#ifdef SPI_ASYNC_USE
    #ifndef SPI_DMA_USE
        #error "SPI_ASYNC_USE requires SPI_DMA_USE to be defined!"
    #endif
#endif
typedef enum
{
    SPI_INIT,
    SPI_RDY,
    SPI_IDLE,
    SPI_BUSY,
    SPI_ERR,
    SPI_UNRDY,
}spiState;
struct spi_controller
{
    uint8_t nr;                             /*SPI外设的编号*/
    spiState spi_state;
	/*要注意中断中不能使用互斥量 所以这个不能是互斥锁哦*/
	/*所以就要自己注意优先级翻转问题了*/
    SemaphoreHandle_t spi_lock;       /*互斥 保证同一时间只有一个可以访问*/
    bool asynchronous;
    SPI_HandleTypeDef spi_handler; 
#ifdef SPI_DMA_USE
    DMA_HandleTypeDef dma_tx;
    DMA_HandleTypeDef dma_rx;
    SemaphoreHandle_t spi_sync;       /*实现同步/异步的手段*/
#endif
    /*在异步传输时 要保证对应的buf在异步传输完成前都不能被修改*/
    /*而且要在异步传输完成后取消片选*/
#ifdef SPI_ASYNC_USE	
    spi_callback_t spi_callback;            /*异步传输完成后的回调 */
    void* callback_data;                    /*异步传输需要传入的变量*/
#endif
};
  • 同步和异步传输
      最大的不同在于spi_lock的释放时机,对于同步调用,等到传输结束就直接释放就行。对于异步调用,互斥锁的释放时机就放到了中断传输完成的服务程序中。
    static uint8_t spi_transfer(uint8_t nr, uint8_t* tx_buf, uint8_t* rx_buf, uint16_t size)
    {
        struct spi_controller* ctrl = spi_controller_get(nr);
        HAL_StatusTypeDef state = HAL_ERROR;
        if (ctrl)
        {
    #ifdef SPI_DMA_USE
            state = HAL_SPI_TransmitReceive_DMA(&ctrl->spi_handler, tx_buf, rx_buf, size);
    #else
            /*这个传输就是同步的 没办法用作异步*/
            /*所以对它而言spi_sync这个信号量也没有意义*/
            state = HAL_SPI_TransmitReceive(&ctrl->spi_handler, tx_buf, rx_buf, size, HAL_MAX_DELAY);
    #endif
        } 
        return state;
    }
    static inline void spi_lock(struct spi_controller* ctrl,
                        bool asynchronous,
                        spi_callback_t callback,
                        void *callback_data)
    {
        xSemaphoreTake(ctrl->spi_lock, portMAX_DELAY);
        ctrl->spi_state = SPI_BUSY;
        ctrl->asynchronous = asynchronous;
    #ifdef SPI_ASYNC_USE
        ctrl->spi_callback = callback;
        ctrl->callback_data = callback_data;
    #endif /* CONFIG_SPI_ASYNC */
    }
    static inline void spi_unlock(struct spi_controller* ctrl)
    {
        if (ctrl->asynchronous)     /*异步要等待整个传输完成再释放互斥锁*/
        {
            return;
        }
        xSemaphoreGive(ctrl->spi_lock);
    }
    static inline uint8_t spi_wait_for_completion(struct spi_controller* ctrl)
    {
        BaseType_t rtn = pdTRUE;
        if (!ctrl->asynchronous)    /*同步模式且还用了DMA就得等啦 */
        {
            #ifdef SPI_DMA_USE
                rtn = xSemaphoreTake(ctrl->spi_sync, portMAX_DELAY);
            #endif
        }
        return rtn;
    }
    uint8_t transceive(uint8_t nr, uint8_t *tx_bufs, uint8_t *rx_bufs, uint16_t size, bool asynchronous, spi_callback_t cb, void *userdata)
    {
        struct spi_controller* ctrl = spi_controller_get(nr);
        uint8_t error = 0;
        if (ctrl)       /*片选过程放在传输之外了 不论同步还是异步*/
        {
            /*上锁*/
            spi_lock(ctrl, asynchronous, cb, userdata);
            /*传输*/
            error = spi_transfer(nr, tx_bufs, rx_bufs, size);
            configASSERT(error == HAL_OK);
            /*等待 */
            spi_wait_for_completion(ctrl);
            /*解锁 */
            spi_unlock(ctrl);
        }
        return error;
    }
    void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) 
    {
        struct spi_controller* ctrl = container_of(hspi, struct spi_controller, spi_handler);
        BaseType_t xHigherPriorityLock = pdFALSE;
        BaseType_t xHigherPrioritySync = pdFALSE;
        #ifdef SPI_DMA_USE
            if (!ctrl->asynchronous)
            {
                xSemaphoreGiveFromISR(ctrl->spi_sync,&xHigherPrioritySync);
            }
        #endif
            if (ctrl->asynchronous)
            {
            #ifdef SPI_ASYNC_USE 
                if (ctrl->spi_callback)
                    ctrl->spi_callback(ctrl->callback_data);
            #endif
                xSemaphoreGiveFromISR(ctrl->spi_lock,&xHigherPriorityLock);
            }
            ctrl->spi_state = SPI_IDLE;
            portYIELD_FROM_ISR((xHigherPrioritySync || xHigherPriorityLock));
    }
    

SDIO协议封装–wifi_sd_drv.c

  主要是SD协议的初始化和SD卡命令发送的封装sd_send_cmd()

WIFI底层驱动代码编写–wifi_transfer.c

  因为WIFI模块和开发板之间传输数据主要依赖的是CMD52和CMD53命令,所以这里依据手册中对CMD52命令和CMD53命令的介绍做了进一步的封装。另外就是数据接收和发送的封装,因为SDIO的数据传输是有要求的(起始信号 CRC校验等等),所以封装两个函数WiFi_LowLevel_ReadData()和WiFi_LowLevel_WriteData()传输数据

/**
 * @brief       WIFI模块写入数据
 * @param       func:分区编号
 * @param       addr:写入wifi模块的起始位位置
 * @param       data:要写入WIFI模块数据的起始地址
 * @param       size:写入数据的大小
 * @param       bufsize:对于data而言,整个发送缓冲区的大小(size <= bufsize)
 * @retval      传输状态
 */
uint8_t WiFi_LowLevel_WriteData(uint8_t func, uint32_t addr, 
	void *data, uint32_t size, 
	uint32_t bufsize, uint32_t flags)
{
    if(sd_select())
    {
        printf("在WiFi_LowLevel_WriteData选中失败\r\n");
        return WIFI_TF_ERR;
    }

    uint8_t  *p = (uint8_t * )data;
    int ret;
    char err = 0;
    uint8_t resp[2];
    uint16_t block_num; 
    uint16_t crc = 0xffff;
    uint32_t curr;
    uint32_t cmd53_flags = CMD53_WRITE;

    if ((uintptr_t)data & 3)
    {
        printf("%s: data must be 4-byte aligned!\n", __FUNCTION__);
        sd_deselect();
        return WIFI_TF_ERR; 
    }
    if (size == 0 || data == NULL)
    {
        printf("%s: size cannot be 0!\n", __FUNCTION__);
        sd_deselect();
        return WIFI_TF_ERR;
    }
    // 大小都不够用块模式就不用
    block_num = WiFi_LowLevel_GetBlockNum(func, &size, flags);
    if (bufsize != 0 && bufsize < size) 
    {
        printf("%s: a buffer of at least %d bytes is required! bufsize=%d\r\n", __FUNCTION__, size, bufsize);
    }
    if (flags & WIFI_RWDATA_ADDRINCREMENT)
    {
        //printf("地址需要自增\r\n");
        cmd53_flags |= CMD53_INCREMENTING; //地址要不要自增
    }
        //printf("地址不需要自增\r\n");
    // 按照块模式发送
    if (block_num)
        ret = WiFi_LowLevel_SendCMD53(func, addr, block_num, cmd53_flags | CMD53_BLOCKMODE, resp, sizeof(resp));
    // 按字节发送
    else
        ret = WiFi_LowLevel_SendCMD53(func, addr, size, cmd53_flags, resp, sizeof(resp));
    if (ret == SD_TIMEOUT)
    {
        sd_deselect();
        printf("返回值是超时寄寄了\r\n");

        return 0;
    }
    sd_spi_read_write_byte(0xff); // NWR �?能就�?占位�?
    while (size > 0 && err == 0)
    {
        //printf("count:%d\r\n",count++);
        curr = (block_num) ? sdio_block_size[func] : size;
        //printf("curr:%d block_size:%d size:%d\r\n",curr,sdio_block_size[func],size);
        sd_spi_read_write_byte(0xfc);       // start block token
        WiFi_LowLevel_Send(p, curr);
        crc = CRC16_CCITT(p, curr);         //这个CRC校验 
        sd_spi_read_write_byte((crc >> 8) & 0xFF); // 提取高8位
        sd_spi_read_write_byte(crc & 0xff);				//提取低8位
        resp[0] = sd_spi_read_write_byte(0xff); // start block token
        //实际上你会发现crc校验还是失败
        //现在就是发送过去了
        // 用硬件SPI就会变得非常慢
        if(resp[0] == 0xe5 || resp[0] == 0xeb)
        {
            //printf("resp is %2x\r\n",resp[0]); 
            p += curr;
            size -= curr;
        }
        else
        {
            printf("传输错误%2x 此时curr:%d\r\n",resp[0],curr);
            sd_deselect();
            return WIFI_TF_ERR;
        }

    }
    sd_deselect();
    return err == 0 ? WIFI_TF_OK : WIFI_TF_ERR;
}

WIFI底层驱动代码编写–wifi_drv.c

  wifi模块的初始化工作,包括加载固件,获取MAC地址,WIFI的四次握手等功能。
  这里的话有个常用的紧凑类型结构体+指针类型转换的设计思想。因为接受到的数据都是数据流(uint8_t类型的数组),那怎么快速从数组中得到想要的信息呢?可以先依据通信帧的结构封装一个紧凑结构体(因为不紧凑的话结构体之间有间隙就不行了),然后把数组转成这个指针类型进行数据访问。这种把通信帧格式转换成结构体的思想可以帮我们快速封装数据帧和解析数据帧。
  这也是这个模块速度起不来的原因之一,发送数据每一块都得CRC校验外加还得有回应(QAQ)

/******************************************************************************
 *	函数名:	mrvl88w8801_mac_addr_prepare
 * 参数:  		tx(IN)				-->tx buffer
 				cmd_action(IN)	-->set/get
 				data_buff(IN)		-->mac address,此部分主要用于set
 				data_len(IN)		-->mac的长度
 * 返回值: 	返回执行结果
 * 描述:		组HostCmd_CMD_802_11_MAC_ADDRESS command的封包
******************************************************************************/
static uint8_t mrvl88w8801_mac_addr_prepare(uint8_t* tx,uint16_t cmd_action,void *data_buff,uint16_t data_len)
{
    HostCmd_DS_COMMAND *cmd = (HostCmd_DS_COMMAND *)tx; /*这个结构体通过NAME_PACKED强制对齐了*/
    HostCmd_DS_802_11_MAC_ADDRESS *pmac_addr = &cmd->params.mac_addr;
    uint16_t tx_packet_len = CMD_HDR_SIZE + sizeof(HostCmd_DS_802_11_MAC_ADDRESS)+data_len;

    cmd->pack_len = tx_packet_len;
    cmd->pack_type = TYPE_CMD_CMDRSP;
    cmd->command = HostCmd_CMD_802_11_MAC_ADDRESS;
    cmd->size = tx_packet_len - CMD_SDIO_HDR_SIZE;
    cmd->seq_num = 0;
    cmd->bss = 0;
    cmd->result = 0;
    if(cmd_action == HostCmd_ACT_GEN_GET)
    {
        pmac_addr->action = HostCmd_ACT_GEN_GET;
        memset(pmac_addr->mac_addr,0,MAC_ADDR_LENGTH);
    }
    else	/* set mac address */
    {
        pmac_addr->action = HostCmd_ACT_GEN_SET;
        memcpy(pmac_addr->mac_addr,data_buff,MAC_ADDR_LENGTH);
    }
    return COMP_ERR_OK;
}

具体的,关于这个模块的更加详细的介绍,可以参考博客:https://blog.csdn.net/xiaoxiaopengbo/category_9619031.html

WIFI驱动代码开发–wifi.c

  当我们完成了整个WIFI模块的底层驱动开发后,现在的问题是怎么样让wifi的驱动更加高效,要解决以下几个问题:

    1. 如何与上层的LWIP协议栈交互
    1. WIFI模块本身的驱动方式,是不断轮训还是中断
        在这里我的想法是抽象出一个wifi线程,对于LWIP协议栈,当它有数据要发送的时候,就把数据塞入到FIFO中,然后通过消息队列通知wifi线程。而对于接受,则采用中断的方式(可以配置WIFI模块的上传中断)。当WIFI模块接收到数据后,拉低对应引脚,然后在中断处理程序中也通过消息队列通知wifi线程解析数据,解析成功就向上提交给LWIP协议栈。
/**
 * @brief       wifi_task(处理WIFI模块的事件)
 * @param       void
 * @retval      void
 */
static void wifi_task(void)
{

	wifi_msg wifi_status = {0};
	/* 初始化必要的变量 */
	wifi_recvmsg_queue = xQueueCreate(16,sizeof(wifi_msg)); 
	configASSERT(wifi_recvmsg_queue);
#if 0
	recv_fifo = kfifo_alloc(WIFI_FIFO_SIZE);
    configASSERT(recv_fifo);
	g_rx_queue = xQueueCreate(16,sizeof(uint32_t));
	configASSERT(g_rx_queue != NULL);
#endif
	send_fifo = kfifo_alloc(WIFI_FIFO_SIZE);
	configASSERT(send_fifo);
	// LWIP协议栈的初始化
	printf("lwip init\r\n");
	while (lwip_common_init(mac_array) != 0)	
    {
		printf("lwip init fail\r\n");
        delay_ms(500);
        LED1_TOGGLE();
		delay_ms(500);
    }
	printf("lwip init success\r\n");
	//sd_irq_exit_enable();			/*启用中断(实际上没法用 因为不可能在中断清除寄存器(会造成阻塞))*/	
	// 等WIFI的数据处理线程创建完才能创建上层的LWIP应用线程
	lwip_demo_init(LWIP_TASK_TYPE);						
	printf("enter wifi thread handle data\r\n");
	// 这里其实保险加一个time_out
    while(1)
    {
        if(xQueueReceive(wifi_recvmsg_queue, &wifi_status, 5) == pdTRUE)
        {
			// 不同的状态调用不同的处理函数
            switch(wifi_status.wifi_status)
			{
				case WIFI_IRQ:						// 理想状态是通过中断而不是现在的轮询
					mrvl88w8801_process_packet();
					break;
				case WIFI_SEND:	
					wifi_send_data(wifi_status.init_status);	// 需要发送数据的长度
					break;
				default:
					break;
			}
        }	
		mrvl88w8801_process_packet(); /*超时处理,万一接收中断上报不及时*/
	}
}
/*只是延时*/
void EXTI15_10_IRQHandler(void)
{
	wifi_msg wifi_status;
    // 检查是否是PC13引脚触发的中断
    if(__HAL_GPIO_EXTI_GET_IT(SD_IRQ_PIN) != RESET)
    {
        __HAL_GPIO_EXTI_CLEAR_IT(SD_IRQ_PIN);
		BaseType_t xHigherPriorityTaskWoken = pdFALSE;
		wifi_status.wifi_status = WIFI_IRQ;
		xQueueSendFromISR(wifi_recvmsg_queue, &wifi_status, &xHigherPriorityTaskWoken);
		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
	}
}

  理想很丰满,现实很骨感。最让我难受的就是这个接收中断的上报,这个引脚的电平总会抽风(QAQ),可能是驱动能力太弱需要外接(上/下拉电阻而我没这个能力)。反正方案仅供参考。
  而且我在这里开发的时候有一个疑问,那就是关于这个中断上报,按照数据手册说的是这样 有数据–>电平拉低–>清除寄存器–>电平变高。 但是问题是啥呢,清除寄存器这个动作本身是基于SDIO协议的CMD52命令,那肯定不能放在中断中对吧(会阻塞的)。而且万一我清除寄存器的时候刚好有新数据来了怎么办,电平还会拉高吗?那我也不知道得看模块自身的驱动代码。so硬件和软件都可能有问题,也无法正常排查了(QAQ)

LWIP协议栈的移植

  LWIP协议栈的整体了解可以看这篇博客:https://blog.csdn.net/qq_45731845/article/details/145420389?spm=1011.2415.3001.5331
这里结合这个模块可以再介绍一下

  • netif的初始化
    这里netif的网卡地址得等WIFI模块初始化完毕,通过命令查询到WIFI模块的MAC地址才能够初始化。另外就是自身IP地址,网关的初始化,包括回调函数的初始化等等。
  • 数据的发送:low_level_output
      这里需要做的就是把数据从pbuf中取出来放入kfifo中,并通知wifi线程处理数据
void lwip_low_level_ouput(struct pbuf *p)
{
    uint32_t send_len = kfifo_put_pbuf(send_fifo, p, p->tot_len, 0);

    if (send_len)
    {
		configASSERT(send_len == p->tot_len);
		//printf("low_level_output send success pbuf len:%d send len:%d\r\n", p->tot_len, send_len);
        wifi_msg wifi_status;
        // 制造消息 然后把消息发送出去
        wifi_status.wifi_status = WIFI_SEND;
        wifi_status.init_status = send_len;
        if (xQueueSendToBack(wifi_recvmsg_queue,&wifi_status,0) == pdPASS)
		{
		}
		else
		{
			printf("send to wifi recv queue fail\r\n");
		}
    }
    else
    {
        printf("low_level_output send fail pbuf len:%d send len:%d\r\n", p->tot_len, send_len);
    }
}
  • 数据的接收
/******************************************************************************
 *	函数名:	mrvl88w8801_process_data
 * 参数:  		rx_buffer(IN)			-->rx buffer
 				len(IN)				-->rx buffer len
 * 返回值: 	返回执行结果
 * 描述:		处理收到的data(也就是tcp/ip data)
******************************************************************************/
static uint8_t mrvl88w8801_process_data(uint8_t *rx_buffer,int len)
{
		
    if(pmrvl88w8801_core->bss_type == BSS_TYPE_UAP)
    {
		//printf("进入到bss_type_uap中了\r\n");
        RxPD *rx_packet = (RxPD *)rx_buffer;
        uint16_t rx_payload_len = rx_packet->rx_pkt_length;
        uint8_t *payload = (uint8_t *)((uint8_t *)rx_packet + rx_packet->rx_pkt_offset + CMD_SDIO_HDR_SIZE);
        RxPacketHdr_t *rx_hdr = (RxPacketHdr_t *)payload;

#if 0
        COMP_DEBUG("Rx dest %02x:%02x:%02x:%02x:%02x:%02x\n",
                   rx_hdr->eth803_hdr.dest_addr[0], rx_hdr->eth803_hdr.dest_addr[1],
                   rx_hdr->eth803_hdr.dest_addr[2], rx_hdr->eth803_hdr.dest_addr[3],
                   rx_hdr->eth803_hdr.dest_addr[4], rx_hdr->eth803_hdr.dest_addr[5]);

        COMP_DEBUG("local mac %02x:%02x:%02x:%02x:%02x:%02x\n",
                   pmrvl88w8801_core->mac_address[0], pmrvl88w8801_core->mac_address[1],
                   pmrvl88w8801_core->mac_address[2], pmrvl88w8801_core->mac_address[3],
                   pmrvl88w8801_core->mac_address[4], pmrvl88w8801_core->mac_address[5]);
#endif
        if (rx_hdr->eth803_hdr.dest_addr[0] & 0x01)
        {
            /* 1.广播封包,转发 */
            //printf("广播封包\r\n");
            uint8_t *buffer = mrvl88w8801_get_send_data_buf();
            uint8_t *payload = (uint8_t *)((uint8_t *)rx_packet + rx_packet->rx_pkt_offset + 4);
            comp_memcpy(buffer,payload,rx_payload_len);
            mrvl88w8801_send_data(buffer,rx_payload_len);
            /* 2.自己AP处理广播封包 */
            ethernet_lwip_process(rx_buffer,len);
        }
        else
        {
            /* 单播封包 */
            if(memcmp(pmrvl88w8801_core->mac_address,rx_hdr->eth803_hdr.dest_addr,MAC_ADDR_LENGTH) == 0)
            {
                                //printf("单播封包\r\n");
                ethernet_lwip_process(rx_buffer,len);
            }
            else
            {
                /* 判断是否是STA地址 ,转发 */
                //printf("STA转包\r\n");
                uint8_t *buffer = mrvl88w8801_get_send_data_buf();
                uint8_t *payload = (uint8_t *)((uint8_t *)rx_packet + rx_packet->rx_pkt_offset + 4);
                comp_memcpy(buffer,payload,rx_payload_len);
                mrvl88w8801_send_data(buffer,rx_payload_len);
            }
        }
    }

    if(pmrvl88w8801_core->bss_type == BSS_TYPE_STA)
    {
		//printf("进入到bss_type_sta中了 %d\r\n",len);
        ethernet_lwip_process(rx_buffer,len);
    }
    return COMP_ERR_OK;
}
// WIFI_DRV和LWIP交互
// 在mrvl88w8801_recv_data最终调用
void ethernet_lwip_process(uint8_t *rx,uint32_t rx_len)
{
	RxPD *rx_packet = (RxPD *)rx;  //注意这个state在调用low_level_input之前被修改了
	uint8_t *payload = (uint8_t *)rx_packet + rx_packet->rx_pkt_offset + 4;
    uint16_t payload_len = rx_packet->rx_pkt_length; 
	struct netif  *wifi_netif = ethernetif_get_netif();
	if (!wifi_netif || !wifi_netif->input) 
	{
        return;
    }
    struct pbuf *p = pbuf_alloc(PBUF_RAW, payload_len, PBUF_POOL);
    if (p == NULL) 
	{
        return;
    }
    if (pbuf_take(p, payload, payload_len) != ERR_OK) 
	{
        pbuf_free(p);
        return;
    }
	taskENTER_CRITICAL();
	/* 调用netif结构体中的input字段(一个函数)来处理数据包 */
	if (wifi_netif->input(p, wifi_netif) != ERR_OK)
	{
		LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
		pbuf_free(p);
		p = NULL;
	}
	else
	{
	}
	taskEXIT_CRITICAL();
}

问题

  • SPI传输相关的问题 SPI的模式和I2C的不同等等
    这里补充一下I2C是开漏输出,能输出低电平对外是高阻态,这意味着I2C输出高电平需要时间,所以I2C的传输速率较SPI慢(当然也因为I2C是有格式的,SPI纯数据传输)
  • LWIP相关的问题
    • 计网知识:什么拥塞控制 三次握手 四次挥手 OSI七层结构 http
    • LWIP模块的内存管理:内存池与堆内存
    • LWIP里面的pbuf?讲一下对pbuf的了解?在lwip里充当的一个角色?
    • pbuf是怎么和内存管理交互的?大概讲一下分配pbuf的传递/碎片问题?
    • MQTT协议相关: QoS(怎么保证只有一次) MQTT的通信机制
    • LWIP的调试: 借助wireshark
  • WIFI模块
    • WIFI模块的四次握手
    • 底层驱动是怎么设计的?为什么这么设计?

更多推荐