zoukankan      html  css  js  c++  java
  • 【框架】设备与驱动的拆分及实现-I2C


    前言

    • 本笔记主要传达一种设备驱动拆分的概念和实现。
    • 使得写好一个驱动框架后,随意添加相应设备,提高开发效率。
    • 使用到以空间换时间的方法,即是数组管理设备,使得时间复杂度为 O(1)。(数组直接定位)。
    • 本笔记的框架支持 N个设备 绑定 X个驱动

    笔录草稿

    • 驱动ID 就是 驱动数组下标,
    • 设备ID 就是 设备数组下标,
    • 访问 驱动数据或设备数据 都采用 ID 访问。

    概要

    • 触发想法
      • 有时候,在写驱动时,发现多个设备使用同一个驱动逻辑,只是部分内容不一样(如引脚),此时就可以想如何写出一个驱动逻辑支持多个不同设备。
    • 例子:IIC
      • 一个 IIC 逻辑
      • 多个设备绑定 IIC
      • 目标效果:
        • 只需要执行以下步骤即可: 注册 IIC 驱动 --> 注册实际设备A并绑定 IIC --> 初始化该 IIC
        • 只需要执行以下步骤即可: 注册 IIC 驱动 --> 注册实际设备B并绑定 IIC --> 初始化该 IIC

    原理及实现方法

    • ID 为数组下标,可以根据 ID 获得 驱动或设备 句柄。(LiteOS 里任务ID和任务句柄也类似噢)

    • 数组为 驱动数组或设备数组或其它需要统一管理的数组等等。主要为实体开辟空间,直接定位使用。

      • 使用数组管理是明显的 空间换时间的方法,时间复杂度达到O(1)
      • 当然也可以使用链表,但是时间复杂度可能达不到 O(1)。
    • 图解

      • 驱动数组

      • 驱动 ID 表

      • 设备数组

      • 设备 ID 表

    • 实现 驱动部分

      1. 创建两个驱动文件:bsp_xx.cbsp_x.h
      2. 创建 xx 驱动名字列表
        • 名字列表也就是 ID,用于下标、校验和操作
          • 下标:数组下标,用于直接定位,获得驱动句柄
          • 校验:下标对应的驱动里面也有保存 驱动 ID 的,在使用时,通过对比操作带来的ID与结构体里面的ID是否相等即可检查到是否获得准确的驱动实体
          • 操作:通过 ID 获得驱动句柄,便可进行操作
      3. 组建 xx 驱动结构体
        • xx 驱动结构体里面
          1. 必须包含 驱动 ID
          2. 其他业务成员
      4. 编写 注册 xx 驱动函数
        • 注册 xx 驱动函数 其实就是一个初始化,初始化 驱动ID 对应驱动数组下标的实体驱动
        • 必须给对应实体驱动里的 驱动ID 赋 当前 ID 值,这样使用时便可以校验
      5. 创建 xx 驱动数组
        • xx 驱动数组 就是所有驱动实体的空间,不同下标对应不同的实体驱动
        • 使用到数组,即是静态申请空间。当然也可以自己实现动态申请,如用链表的方法或者动态申请内存空间。
      6. 编写驱动逻辑
        • 一个驱动,支持多个设备
        • 驱动逻辑,多个设备的驱动逻辑相似,不同点可以通过 驱动结构体 中的成员区别开来。
    • 实现 设备部分

      1. 创建两个设备文件:lss_yy.clss_yy.h
      2. 创建 yy 设备名字列表
        • 名字列表也就是 ID,用于下标、校验和操作
          • 下标:数组下标,用于直接定位,获得驱动句柄
          • 校验:下标对应的设备里面也有保存 设备 ID 的,在使用时,通过对比操作带来的ID与结构体里面的ID是否相等即可检查到是否获得准确的设备实体
          • 操作:通过 ID 获得设备句柄,便可进行操作
      3. 组建 xx 设备结构体
        • xx 设备结构体里面
          1. 必须包含 设备 ID : 用于标识本结构体为哪一个设备
          2. 必须包含 驱动 ID : 就是绑定的 驱动 ID
          3. 其他业务成员
      4. 编写 注册 xx 设备函数
        • 注册 xx 设备函数 其实就是一个初始化,初始化 设备ID 对应驱动数组下标的实体设备
        • 必须给对应实体驱动里的 驱动ID 赋 当前 ID 值,这样使用时便可以校验
      5. 创建 xx 设备数组
        • xx 设备数组 就是所有设备实体的空间,不同下标对应不同的实体设备
        • 使用到数组,即是静态申请空间。当然也可以自己实现动态申请,如用链表的方法或者动态申请内存空间。
      6. 编写设备逻辑
        • 在设备逻辑中,通过 设备ID和设备数组 获得设备实体,再在设备实体中找到驱动ID,把设备ID传给驱动逻辑函数即可。
      7. 实现设备初始化函数 **
        • 简要步骤(必须遵循前三个步骤的顺序
          1. 先注册 xx 驱动
          2. 注册 yy 设备,并绑定对应的 xx 驱动
          3. 初始化 xx 引脚
          4. 执行自己的驱动业务

    IIC 例子实战-驱动

    • 通过实现一下步骤,我们便实现了 设备驱动框架的驱动部分
    • 简要步骤
      1. 创建两个文件:bsp_i2c.cbsp_i2c.h
      2. 创建 I2C 驱动名字列表
      3. 组建 I2C 驱动结构体
      4. 编写 注册 I2C 驱动函数
      5. 创建 I2C 驱动数组
      6. 编写驱动逻辑
        1. static uint32_t selectClkByGpio(const uint32_t addr) 选择时钟信号函数
        2. void i2cGpioInit(eI2C_ID id) I2C 引脚初始化函数
        3. void i2cStart(eI2C_ID id) I2C Start 函数
        4. void i2cStop(eI2C_ID id) I2C Stop 函数
        5. uint8_t i2cSendByte(eI2C_ID id, uint16_t TxData) I2C SendByte 函数
        6. uint8_t i2cReceiveByte(eI2C_ID id) I2C ReceiveByte 函数
        7. void i2cAck(eI2C_ID id, uint8_t Ack) I2C Ack 函数
        8. uint8_t i2cWaitAck(eI2C_ID id) I2C WaitAck 函数

    1. 创建文件

    • 创建两个文件:bsp_i2c.cbsp_i2c.h

    2. 创建 I2C 驱动名字列表

    • 本驱动列表需要根据实际设备修改
    • 驱动名字其实就是对应驱动数组下标,用于直接定位
    • 注意:
      • 第一个驱动名必须从 0 开始
      • ei2cDEVICE_COUNT 是和 i2cI2C_DEVICE_COUNT 一样的大小,在实际工程中,二选一即可。
    • 源码例子如下,驱动名字按照自己的命名风格命名即可。
    /*
    *********************************************************************************************************
    *                                                 CONFIG 
    *********************************************************************************************************
    */
    // [注][I2C] 根据实际设备修改
    // i2c 驱动数量
    #define i2cI2C_DRIVER_COUNT 3
    /**
    * @brief  i2c id
    * @author lzm
    */
    typedef enum
    {
        ei2cEEPROM_1 = 0, // 第一个 EEPROM 设备驱动
        ei2cEEPROM_2, // 第二个 EEPROM 设备驱动
        ei2cMPU6050, // MPU6050设备驱动
        
        ei2cDEVICE_COUNT; // 驱动数量
    }eI2C_ID;
    

    3. 组建 I2C 驱动结构体

    • I2C 驱动结构体必须包含
      1. I2C ID : 就是一个实体 I2C 的 ID驱动数组下标
      2. SCL 及 SDA 引脚数据。
    • 结构体中的延时数据,主要是为了 IIC 速度可控。
    /*
    *********************************************************************************************************
    *                                                 BASIC
    *********************************************************************************************************
    */
    /**
    * @brief  i2c struct
    * @author lzm
    */
    struct I2C_T{
        /* id */
        eI2C_ID ID;
        
        /* delay */
        // cnt
        unsigned char delayUsCnt;
        // delay function
        void ( *delayUsFun )(int cnt);
        
        /* pin */
        GPIO_TypeDef *  sclGpiox;
        uint16_t                 sclPin;
        GPIO_TypeDef *  sdaGpiox;
        uint16_t                 sdaPin;
    };
    typedef struct I2C_T i2c_t;
    

    4. 编写-注册 I2C 驱动函数

    • 注册 I2C 驱动函数 其实就是初始化对应驱动的参数,如绑定 SCL 和 SDA 引脚。
    • 在开发中,实际设备绑定及使用 I2C 之前必须先注册对应 I2C 驱动。
    • 一些参数解析
      • @param delayuscnt : 延时多少个 微妙
      • @param fun : 微妙延时函数
      • @param sclgpio : SCL 引脚 port
      • @param sclpin : SCL 引脚 pin
      • @param sdagpio : SDA 引脚 port
      • @param sdapin : SDA 引脚 pin
    /*
    *********************************************************************************************************
    *                                                 DEFINE [API] FUNCTION
    *********************************************************************************************************
    */
    /**
      * @brief  注册IIC设备
      * i2cDeviceElem[i2cID].id = i2cID; // 保持下标与ID相等,查找时可以直接定位,实现时间复杂度为O(1);
      * @param 
      * @retval none
      * @author lzm
      */
    #define REGISTER_I2C_DRI(i2cID, delayuscnt, fun, sclgpio, sclpin, sdagpio, sdapin) 
    { 
        i2cDeviceElem[i2cID].id = i2cID; 
        i2cDeviceElem[i2cID].delayUsCnt = delayuscnt; 
        i2cDeviceElem[i2cID].delayUsFun = fun; 
        i2cDeviceElem[i2cID].sclGpiox = sclgpio; 
        i2cDeviceElem[i2cID].sclPin = sclpin; 
        i2cDeviceElem[i2cID].sdaGpiox = sdagpio; 
        i2cDeviceElem[i2cID].sdaPin = sdapin; 
    }
    

    5. 创建 I2C 驱动数组

    • i2cI2C_DRIVER_COUNT 表示有 i2cI2C_DRIVER_COUNT 个 I2C 驱动
    • 创建 I2C 驱动数组是提前为可能需要用到 I2C 驱动的设备提前申请空间(静态),当然也可以动态申请。
    /*
    *********************************************************************************************************
    *                                                 DEFINE
    *********************************************************************************************************
    */
    // i2c 驱动元素(设备表)
    i2c_t i2cDriverElem[i2cI2C_DRIVER_COUNT];
    

    6. 编写驱动逻辑

    static uint32_t selectClkByGpio(const uint32_t addr) 选择时钟信号函数
    • 本函数主要用于根据引脚端口来选择时钟,当然也可以选择把 时钟变量 放到 I2C 驱动结构体里面
    • 形参: const uint32_t addr 需要初始化引脚对应的 port
    • 返回:返回时钟值 或 NULL
    /**
      * @brief  选出时钟信号线
      * @param addr : 引脚对应 port
      * @retval 返回时钟值 或 NULL
      * @author lzm
      */
    static uint32_t selectClkByGpio(const uint32_t addr)
    {
        switch(addr)
        {
            case GPIOA_BASE:
                return RCC_APB2Periph_GPIOA;
            case GPIOB_BASE:
                return RCC_APB2Periph_GPIOB;
            case GPIOC_BASE:
                return RCC_APB2Periph_GPIOC;
            case GPIOD_BASE:
                return RCC_APB2Periph_GPIOD;
            case GPIOE_BASE:
                return RCC_APB2Periph_GPIOE;
            case GPIOF_BASE:
                return RCC_APB2Periph_GPIOF;
            case GPIOG_BASE:
                return RCC_APB2Periph_GPIOG;
        }
        return NULL;
    }
    
    void i2cGpioInit(eI2C_ID id) 初始化I2C引脚
    • 本函数主要用于初始化 I2C 需要的引脚:SCL 和 SDA
    • 形参: eI2C_ID id 为 I2C 驱动 ID,可以理解为需要初始化哪一个 I2C 驱动,从 I2C 驱动命名表中选出。
    • 返回:无
    • 分析
      • 原理:I2C 驱动 ID 即是 I2C 驱动数组下标,对应一个 I2C 驱动,通过 ID 可以获取 I2C 数据,然后做出处理。
      • 步骤:
        1. 获取需要初始化的时钟值 sclGpioClksdaGpioClk
        2. 初始化需要的时钟
        3. 配置初始化引脚结构体并初始化
        4. 拉高 SCL 和 SDA引脚。
    /**
      * @brief  初始化I2C引脚
      * @param id : I2C 驱动 ID
      * @retval none
      * @author lzm
      */
    void i2cGpioInit(eI2C_ID id)
    {
        GPIO_InitTypeDef G_GPIO_IniStruct;  //定义结构体
    	uint32_t               sclGpioClk;
        uint32_t               sdaGpioClk;
        const i2c_t *         i2c = &i2cDeviceElem[id];
        
        sclGpioClk = selectClkByGpio((uint32_t)(i2c->sclGpiox));
        sdaGpioClk = selectClkByGpio((uint32_t)(i2c->sdaGpiox));
        
    	RCC_APB2PeriphClockCmd(sclGpioClk | sdaGpioClk, ENABLE);  //打开时钟
        
        G_GPIO_IniStruct.GPIO_Pin = i2c->sclPin;     //配置端口及引脚(指定方向)
        G_GPIO_IniStruct.GPIO_Mode = GPIO_Mode_Out_OD;
        G_GPIO_IniStruct.GPIO_Speed =  GPIO_Speed_50MHz;	     
        GPIO_Init(i2c->sclGpiox, &G_GPIO_IniStruct);    //初始化端口(开往指定方向)
        
        G_GPIO_IniStruct.GPIO_Pin = i2c->sdaPin;     //配置端口及引脚(指定方向)
        G_GPIO_IniStruct.GPIO_Mode = GPIO_Mode_Out_OD;
        G_GPIO_IniStruct.GPIO_Speed =  GPIO_Speed_50MHz;	     
        GPIO_Init(i2c->sdaGpiox, &G_GPIO_IniStruct);    //初始化端口(开往指定方向)
        
        // 初始化完以后先拉高
        iicOutHi(i2c->sclGpiox, i2c->sclPin);
        iicOutHi(i2c->sdaGpiox, i2c->sdaPin);
    }
    
    void i2cStart(eI2C_ID id) I2C Start函数
    • 本函数为 I2C 逻辑函数 Start 部分
    • 形参: eI2C_ID id 为 I2C 驱动 ID,可以理解为需要初始化哪一个 I2C 驱动,从 I2C 驱动命名表中选出。
    • 返回:无
    • 分析
      • 原理:I2C 驱动 ID 即是 I2C 驱动数组下标,对应一个 I2C 驱动,通过 ID 可以获取 I2C 数据,然后做出处理。
      • 步骤:
        1. 从驱动表中获取一个驱动的句柄进行操作,i2c_t * i2c = &i2cDeviceElem[id];
        2. 通过句柄获取该 I2C 驱动数据,实现逻辑
    /**
      * @brief  IIC START
      * @param id : I2C 驱动 ID
      * @retval none
      * @author lzm
      */
    void i2cStart(eI2C_ID id)
    {  
       i2c_t * i2c = &i2cDeviceElem[id];
        
        iicSdaOutHi(i2c);
        iicSclOutHi(i2c);
        i2c->delayUsFun(i2c->delayUsCnt);
        iicSdaOutLo(i2c);
        i2c->delayUsFun(i2c->delayUsCnt);
    }
    
    其余 I2C 逻辑函数
    • 其余 I2C 逻辑函数原理和 void i2cStart(eI2C_ID id) 函数原理一样,只是实现的逻辑不一样而已,完整源码可以参考我的gitee上的 LiteOS 源码工程。

    IIC 例子实战-设备

    • 本笔记选用 eeprom 设备做例子
    • 通过实现一下步骤,我们便实现了 设备驱动框架的设备部分
    • 简要步骤
      1. 创建设备文件:lss_eeprom.clss_eeprom.h
      2. 创建设备名字列表
      3. 组键设备结构体
      4. 编写注册设备函数
      5. 创建设备数组
      6. 实现设备驱动逻辑
      7. 实现设备初始化函数

    1. 创建设备文件

    • 直接创建 lss_eeprom.clss_eeprom.h 文件即可。

    2. 创建设备名字列表

    • 本设备列表需要根据实际设备修改
    • 设备名字其实就是对应驱动数组下标,用于直接定位
    • 注意:
      • 第一个设备名必须从 0 开始
      • eeeprom_COUNT 是和 eeEEPROM_DEVICE_COUNT 一样的大小,在实际工程中,二选一即可。
    • 源码例子如下,驱动名字按照自己的命名风格命名即可。
    /*
    *********************************************************************************************************
    *                                                 CONFIG API
    *********************************************************************************************************
    */
    /* [注][eeprom]实时修改 */
    // eeprom 设备数量
    #define eeEEPROM_DEVICE_COUNT 2
    /* delay API */
    #define eeDelayMs(cnt)	vTaskDelay(cnt)             /* 调度式延时 */
    #define eeEEPROM_WRITE_COUNT	 5		        /* 写页时等待时间 */
    
    /* fpga id. */
    typedef enum
    {
        eAT24C08_1 = 0,
        eAT24C08_2,
        
        eeeprom_COUNT,
    }eEEPROM_ID;
    

    3. 组键设备结构体

    • 设备结构体必须包含
      1. eEEPROM_ID ID : 就是一个实体 EEPROM 的 ID设备数组下标
      2. eI2C ID : 就是一个实体 I2C 的 ID驱动数组下标
    • 除了以上两个必须的成员外,其他成员可以根据业务自行添加。
    • 以上两个 ID 是 eEEPROM_ID ID 绑定 eI2C ID ,设备结构体只需要知道它对应哪一个 I2C 实体即可,即是只需要知道一个 I2C ID即可。
    /*
    *********************************************************************************************************
    *                                                 BASIC
    *********************************************************************************************************
    */
    /* eeprom struct */
    struct EEPROM_T{
        /* id */
        eEEPROM_ID ID; 
        /* i2c id */
        eI2C_ID i2cID;
    };
    

    4. 编写注册设备函数

    • 注册设备函数 其实就是初始化一些数据,如绑定 I2C,绑定 SPI,绑定一些数据等等。
    • 在开发中,实际设备绑定及使用 I2C 之前必须先注册对应 I2C 驱动,然后注册 I2C 设备。
    • 一些参数解析
      • @param eeid : EEPROM ID,用于直接定位,也可以同时用于定位校验。
      • @param i2cid : 设备绑定的 I2C 驱动 ID。
    /*
    *********************************************************************************************************
    *                                                 DEFINE [API] FUNCTION
    *********************************************************************************************************
    */
    /**
      * @brief  注册IIC设备
      * @param eeid : EEPROM ID,用于直接定位,也可以同时用于定位校验。
      * @param i2cid : 设备绑定的 I2C 驱动 ID。
      * @retval none
      * @author lzm
      */
    #define REGISTER_EEPROM_DEV(eeid, i2cid) 
    { 
        eepromDeviceElem[eeid].ID = eeid; 
        eepromDeviceElem[eeid].i2cID = i2cid; 
    }
    

    5. 创建 EEPROM 设备数组

    • eeEEPROM_DEVICE_COUNT 表示有 eeEEPROM_DEVICE_COUNT 个 EEPROM 设备
    • 创建 I2C 驱动数组是提前为可能需要用到 I2C 驱动的设备提前申请空间(静态),当然也可以动态申请。
    /*
    *********************************************************************************************************
    *                                                 DEFINE
    *********************************************************************************************************
    */
    // eeprom 设备元素(设备表)
    eeprom_t eepromDeviceElem[eeEEPROM_DEVICE_COUNT];
    

    6. 实现设备驱动逻辑

    • 原理:通过 eI2C_ID i2cid = eepromDeviceElem[id].i2cID; 获取对应的 I2C 驱动实体
    • 例子如下,该函数只需要用设备 ID eEEPROM_ID 管理即可,APP 用户不需接触到 I2C 驱动名字的操作,只需要自己操作的设备的设备名字即可。
    eeprom 其中一个逻辑函数
    • 其余逻辑函数自己可以实现,只需要寻址问题即可。
    /**
      * @brief  read [size] bytes from pReadBuf
      * @param pReadBuf : store data form addr
      *              addr : start addr
      *              size : the size of need read
      * @retval  1 : normal
      *              0 : abnormal
      * @author lzm
      */
    uint8_t __eeReadBytes(eEEPROM_ID id, uint16_t addr, uint8_t *pReadBuf, uint16_t lenght)
    {
    	uint16_t i;
        uint8_t active = 0x0A;
        eI2C_ID i2cid = eepromDeviceElem[id].i2cID;
        
    	while( active-- )
        {
            i2cStart(i2cid);
        
            if (i2cSendByte(i2cid, eeEEPROM_DEVICE_ADDR + ((addr>>8)<<1)))
            {
                i2cStop(i2cid);
                continue;	/* EEPROM器件无应答 */
            }
    #if 0 // [注][eeprom] AT24C32 及以上的 eeprom才启用
            /* High 8 bits address. */
            if(LSS_I2C_SendByte(addr>>8))
            {
                LSS_I2C_Stop();continue;
            }
    #endif
            if (i2cSendByte(i2cid, (uint8_t)(addr)))
            {
                i2cStop(i2cid);
                continue;	/* EEPROM器件无应答 */
            }
                   
            i2cStart(i2cid);
            
            if (i2cSendByte(i2cid, eeEEPROM_DEVICE_ADDR | eeEEPROM_I2C_RD))
            {
                i2cStop(i2cid);
                continue;	/* EEPROM器件无应答 */
            }	
            
            for (i = 0; i < lenght; i++)
            {
                pReadBuf[i] = i2cReceiveByte(i2cid);
                
                if(i == lenght-1) 
                    i2cAck(i2cid,1);     //No ACK
    			else 
                    i2cAck(i2cid,0);     //ACK
            }
            
            i2cStop(i2cid);
            return 0;	/* 执行成功 */
        }
    	return 1;
    }
    

    7. 实现设备初始化函数 **

    • 简要步骤
      1. 先注册 I2C 驱动
      2. 注册 EEPROM 设备,并绑定对应的 I2C 驱动
      3. 初始化 I2C 引脚
      4. 执行自己的驱动业务
    /**
      * @brief  所有EEPROM设备初始化
      * @param 
      * @retval 
      * @author lzm
      */
    void eepromInit(void)
    {
        uint8_t eepromID;
        
        // 先注册 I2C 驱动
        REGISTER_I2C_DRI(ei2cEEPROM_1, 5, dwtDelayUs, EEP_SCL_PORT, EEP_SCL_PIN, EEP_SDA_PORT, EEP_SDA_PIN);
        REGISTER_I2C_DRI(ei2cEEPROM_2, 5, dwtDelayUs, EEP_SCL_PORT, EEP_SCL_PIN, EEP_SDA_PORT, EEP_SDA_PIN);
        // 注册 EEPROM 设备并绑定 i2c 驱动
        REGISTER_EEPROM_DEV(eAT24C08_1, ei2cEEPROM_1);
        REGISTER_EEPROM_DEV(eAT24C08_2, ei2cEEPROM_2);
        
        for (eepromID = 0; eepromID < eeEEPROM_DEVICE_COUNT; eepromID++) 
    	{ 
            // 初始化 I2C
            i2cGpioInit( (eI2C_ID)(ei2cEEPROM + eepromID) );    
            // 业务 [待写]
        }
        // 业务 [待写]
    }
    

    图解例子 **

    • 初始化好 驱动和设备 后,
    • 便可以通过 设备ID 找出 设备句柄
    • 通过 设备句柄 可以知道 设备数据
    • 通过 设备句柄 也可以知道 驱动ID
    • 通过 驱动ID 可以知道 驱动句柄
    • 通过 驱动句柄 也可以知道 驱动数据

    重要后语(小小鸡汤)

    • 自己写 MCU 驱动时想出上述这种框架,感觉很清晰,很精简,开发效率很高,后面才发现和 linux 的设备驱动框架相识。
    • 不过,想出这个框架还是收获满满的。
      • 要学会 偷懒
        • 这里的 偷懒 是提高效率的意思,这不是一件简单的事,还得学会思考。
        • 搭建好一个优秀的框架,后期开发效率高。如上述中添加一个 I2C 设备,直接在设备列表中添加一个枚举,再在设备初始化代码段中注册、绑定即可。
      • 多出去走走
        • 这里也不是让你经常去游山玩水,而是多逛逛一些优秀的论坛、多看看牛人的博客、多研究一下优秀的源码、多了解一下常用的算法、框架等等
          • 本人圈子小,有优秀的学习源,跪求推荐给我哈哈,好东西不怕多
            • 包括技术、理财、外语(英语、日语)、二次元
          • 本人圈子小,有好的学习源,跪求推荐给我哈哈,好东西不怕多
            • 包括技术、理财、外语(英语、日语)、二次元
          • 本人圈子小,有好的学习源,跪求推荐给我哈哈,好东西不怕多
            • 包括技术、理财、外语(英语、日语)、二次元
  • 相关阅读:
    7. Scrapy的高级用法
    6. Scrapy的基本用法
    5. 基于Selenium实现爬虫
    4. 异步爬虫
    3. 数据解析
    2. requests的使用
    1. 爬虫概述
    03-Servlet初识
    Flask框架基础(1)
    登录mysql时,报错ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost' (10061)
  • 原文地址:https://www.cnblogs.com/lizhuming/p/13834535.html
Copyright © 2011-2022 走看看