rt-thread SDIO驱动框架分析(SD卡驱动\SD Nand驱动)
rt-thread SDIO驱动框架分析之SD卡驱动
文章目录
- rt-thread SDIO驱动框架分析之SD卡驱动
- 1. 前言
- 2. SDIO通用驱动框架介绍
- 3. 文件架构分析
- 4. SDIO设备驱动分析
- 5. SDIO设备驱动架构分析
- 6. 调试记录
- 7. 总结
1. 前言
RT-Thread是一款国产化的嵌入式操作系统,目前在嵌入式领域得到广泛应用,其强大的扩展功能以及通用的外设驱动框架备受大家追捧。
关于基本的外设驱动,其官网上基本也都有部分描述,但是关于SDIO设备驱动目前为止还没有相关文档说明,因此本文笔者将根据自己的调试使用经验,与大家分享下rtthread的通用SDIO设备驱动的实现。
RT-Thread github开源地址:https://github.com/RT-Thread/rt-thread
本文基于代码仓库 rt-thread/bsp/stm32/stm32f103-fire-arbitrary 分析代码
- 分支:main
- commit:6808f48bdcf914f03ac757cc19b264a5d0db56de
- 说明:main分支会有不断更新,但是SDIO驱动框架目前应该不会有大变更
硬件介绍:
- 控制器:STM32 基于手上为数不多的野火开发板吧
- SD卡:本次采用的并非SD卡,而是创世CS家的一颗SD Nand, CSNP4GCR01-AMW,有幸申请到了一颗样片
- 这里多说几句,SD nand使用起来和SD卡完全一样,而且SD Nand相比SD卡感觉好用太多,贴片LGA-8封装,和SPI flash 差不多,完美的解决了SD卡松动导致系统不稳定的问题,而且容量又大,个人感觉以后必定是嵌入式存储应用上的主流 (除了价格贵点啥都好,哈哈)想要样片试试水的可以去找深圳雷龙公司官网申请下
2. SDIO通用驱动框架介绍
首先来介绍下 SDIO 通用驱动框架。
RT-Thread 区别于其他操作系统,如FreeRTOS,的一大重要特征是,RT-Thread 中引入了设备驱动框架,并且针对绝大多数外设基本上都已完成对应的设备驱动框架编写,所谓的设备驱动框架,也就是我们所说的建立在应用层与底层驱动层之间的中间件
如下图所示:
应用层:完成业务应用,调用通用接口操作设备驱动层
设备驱动框架层:完成外设通用驱动框架设计,脱离具体的芯片,将驱动中相同部分,如针对SPI,关于SPI的完整读写逻辑等抽离出来
设备驱动层:完成对应芯片的外设驱动程序编写,实现设备驱动框架层的具体接口
对于SDIO外设亦是如此:
- 在设备驱动框架层中,实现SD卡、SDIO卡、MMC卡的通用外设驱动逻辑,如卡的识别、卡的模块切换、卡的读写操作等,这些都是通用的,遵循SD标准协议;
- 在设备驱动层中,根据对应的硬件,完成具体芯片的SDIO外设配置,并实现设备驱动框架层所需要实现的具体接口,如发送CMD命令等。
- 在应用层实现具体的应用,应用层与驱动层解耦
通过这种方式,这样便可以轻松的做到:
- 需要驱动具体的SD、SDIO、MMC时,根据具体的芯片实现对应的SDIO驱动接口即可
- 应用层可直接移植,如出现方案芯片替代时,只需完成设备驱动层适配即可
这也就是RT-Thread让众多开发者疯狂追捧的重大原因了,接下来,我们将具体分析关于SD卡的具体框架层实现,关于SDIO卡、MMC卡,由于使用不多,本文不做深入分析。
3. 文件架构分析
首先我们先来看下SDIO驱动框架有关文件及架构
SDIO驱动框架文件:
SDIO驱动框架文件架构
4. SDIO设备驱动分析
设备驱动与驱动框架文件在不同的目录,设备驱动一般在 bsp
目录中
通常设备驱动完成以下几个事情:
- 初始化具体外设有关数据结构;
- 完成具体外设初始化程序编写;
- 实现设备框架层的具体接口,如:
open
,read
,write
,close
,control
等; - 将具体设备注册到内核中;
需要注意的是,SDIO设备驱动会有些许区别,在SDIO设备驱动程序中,主要完成以下几件事:
- 初始化具体外设有关数据结构;
- SDIO外设的初始化配置;
- 实现设备框架层的以下几个接口:
struct rt_mmcsd_host_ops {void (*request)(struct rt_mmcsd_host *host, struct rt_mmcsd_req *req);void (*set_iocfg)(struct rt_mmcsd_host *host, struct rt_mmcsd_io_cfg *io_cfg);rt_int32_t (*get_card_status)(struct rt_mmcsd_host *host);void (*enable_sdio_irq)(struct rt_mmcsd_host *host, rt_int32_t en); };
- 通知驱动框架层(此处demo程序默认上电前sd卡已接入);
以 rt-thread/bsp/stm32/libraries/HAL_Drivers/drv_sdio.c 程序为例,SDIO驱动层程序从 rt_hw_sdio_init
函数开始,由于使能了自动初始化,此函数由 INIT_DEVICE_EXPORT(rt_hw_sdio_init);
宏实现初始化调用
(关于自动初始化如何实现的细节,可参考笔者另外一篇博文对自动初始化的详细分析:代码自动初始化(点击跳转))
在 rt_hw_sdio_init
函数中,驱动程序主要初始化以下几个结构体:
- stm32外设HAL库配置结构体
SD_HandleTypeDef hsd
- stm32 sdio 设备结构体
struct stm32_sdio_des sdio_des
- sdio硬件外设结构体
struct rthw_sdio *sdio
- mmc sd host结构体
struct rt_mmcsd_host
其关系如下图所示:
结构体数据初始化完成以后,调用 mmcsd_change()
函数,触发框架层逻辑
此外,在设备驱动层提供的操作函数主要有:
static const struct rt_mmcsd_host_ops ops =
{rthw_sdio_request,rthw_sdio_iocfg,rthw_sd_detect,rthw_sdio_irq_update,
};
rthw_sdio_request
实现一次SDIO数据发送rthw_sdio_iocfg
实现SDIO外设配置,注意在SD识别过程中会反复调用,不断更新SDIO外设配置rthw_sd_detect
实现获取卡的状态获取,demo里这里实际没有实现rthw_sdio_irq_update
实现SDIO外设中断的开关配置
函数调用顺序如下:
/* 函数调用顺序 */
rt_hw_sdio_init()-> sdio_host_create(&sdio_des)-> mmcsd_change(host)
5. SDIO设备驱动架构分析
设备驱动架构层,也就是中间层,文件框架如下图所示:
我们首先来看下 mmcsd_core.c
这个文件:
rt_mmcsd_core_init()
初始化函数通过INIT_PREV_EXPORT(rt_mmcsd_core_init);
被初始化调用,同时初始化用于 mmc、sd、sdio检测的邮箱mmcsd_detect_mb
,用于热插拔处理的mmcsd_hotpluge_mb
以及 mmc、sd、sdio检测线程mmcsd_detect_thread
;- 在线程
mmcsd_detect_thread
中,等待mmcsd_detect_mb
邮箱唤醒; - 当SDIO驱动层完成初始化话之后,通过调用
mmcsd_change(host)
函数,将mmcsd_detect_thread
线程唤醒,开始进行mmc、sd卡、sdio卡的识别过程
mmcsd_core_init()
函数内容如下:
int rt_mmcsd_core_init(void)
{rt_err_t ret;/* initialize detect SD cart thread *//* initialize mailbox and create detect SD card thread */ret = rt_mb_init(&mmcsd_detect_mb, "mmcsdmb",&mmcsd_detect_mb_pool[0], sizeof(mmcsd_detect_mb_pool) / sizeof(mmcsd_detect_mb_pool[0]),RT_IPC_FLAG_FIFO);RT_ASSERT(ret == RT_EOK);ret = rt_mb_init(&mmcsd_hotpluge_mb, "mmcsdhotplugmb",&mmcsd_hotpluge_mb_pool[0], sizeof(mmcsd_hotpluge_mb_pool) / sizeof(mmcsd_hotpluge_mb_pool[0]),RT_IPC_FLAG_FIFO);RT_ASSERT(ret == RT_EOK);ret = rt_thread_init(&mmcsd_detect_thread, "mmcsd_detect", mmcsd_detect, RT_NULL,&mmcsd_stack[0], RT_MMCSD_STACK_SIZE, RT_MMCSD_THREAD_PREORITY, 20);if (ret == RT_EOK){rt_thread_startup(&mmcsd_detect_thread);}rt_sdio_init();return 0;
}
INIT_PREV_EXPORT(rt_mmcsd_core_init);
mmcsd_detect()
线程以及mmcsd_change()
函数如下:mmcsd_detect()
函数主要负责完成 SDIO卡、SD卡、MMC卡的初步识别,初步识别确认是哪种类型的卡接入之后,将会调用对应卡驱动文件(SD卡对应sd.c
,SDIO卡对应sdio.c
,MMC卡对应mmc.c
)内的初始化函数,重新完成卡的完整识别流程- 如果对于SD卡识别流程不了解,建议先熟悉SD卡识别流程,参考 SD Nand 与 SD卡 SDIO模式应用流程(点击跳转)
- 具体流程见下述函数描述,对应步骤已补充注释描述
void mmcsd_change(struct rt_mmcsd_host *host)
{rt_mb_send(&mmcsd_detect_mb, (rt_uint32_t)host);
}void mmcsd_detect(void *param)
{struct rt_mmcsd_host *host;rt_uint32_t ocr;rt_int32_t err;while (1){/* 首先等待 mmcsd_detect_mb 信号量,此信号量由 mmcsd_change() 函数发送过来 */if (rt_mb_recv(&mmcsd_detect_mb, (rt_ubase_t *)&host, RT_WAITING_FOREVER) == RT_EOK){/* 通过判断 host->card 确认此次操作是识别卡还是移除卡 */if (host->card == RT_NULL) /* 识别卡 */{mmcsd_host_lock(host); /* 获取锁 */mmcsd_power_up(host); /* 配置SDIO外设电源控制器,power up, 即卡的时钟开启,同时配置SDIO外设时钟为低速模式 */mmcsd_go_idle(host); /* 发送CMD0指令,使卡进入空闲状态 */mmcsd_send_if_cond(host, host->valid_ocr); /* 发送CMD8命令,查询SD卡接口条件 (获取OCR寄存器) *//** 检测SDIO卡使用,SD卡不用管*/err = sdio_io_send_op_cond(host, 0, &ocr); /* 发送CMD5命令,此处是针对SDIO卡使用,SD卡不会响应 */if (!err) /* SD卡不会响应此指令,因此此条件不会成立 */{if (init_sdio(host, ocr))mmcsd_power_off(host);mmcsd_host_unlock(host);continue;}/** 检测SD卡使用,使用SD卡重点关注此项!!!*/err = mmcsd_send_app_op_cond(host, 0, &ocr); /* 发送ACMD41指令(ACMD41:CMD55+CMD41) SD卡将应答此指令 */if (!err){if (init_sd(host, ocr)) /* 此函数内完成SD卡完整的识别流程 */mmcsd_power_off(host); /* 设置SDIO外设,电源关闭,卡的时钟停止 */mmcsd_host_unlock(host); /* 释放锁 */rt_mb_send(&mmcsd_hotpluge_mb, (rt_uint32_t)host); /* 发送邮箱,通知热插拔事件 */continue;}/** 检测MMC卡检测使用,SD卡不用管*/err = mmc_send_op_cond(host, 0, &ocr);if (!err){if (init_mmc(host, ocr))mmcsd_power_off(host);mmcsd_host_unlock(host);rt_mb_send(&mmcsd_hotpluge_mb, (rt_uint32_t)host);continue;}mmcsd_host_unlock(host); /* 识别失败,释放锁 */}else /* 移除卡 */{/* card removed */mmcsd_host_lock(host); /* 获取锁 */if (host->card->sdio_function_num != 0){LOG_W("unsupport sdio card plug out!");}else{rt_mmcsd_blk_remove(host->card);rt_free(host->card);host->card = RT_NULL;}mmcsd_host_unlock(host); /* 释放锁 */rt_mb_send(&mmcsd_hotpluge_mb, (rt_uint32_t)host);}}}
}
- 在
mmcsd_detect()
函数内完成SD卡的初步识别之后,之后将调用sd.c
文件内的init_sd()
函数完成 sd 卡的完整识别过程
/** Starting point for SD card init.*/
rt_int32_t init_sd(struct rt_mmcsd_host *host, rt_uint32_t ocr)
{rt_int32_t err;rt_uint32_t current_ocr;/** We need to get OCR a different way for SPI.*/if (controller_is_spi(host)) /* 判断是否采用SPI模式访问SD卡 */{mmcsd_go_idle(host);err = mmcsd_spi_read_ocr(host, 0, &ocr); if (err)goto err;}if (ocr & VDD_165_195){LOG_I(" SD card claims to support the ""incompletely defined 'low voltage range'. This ""will be ignored.");ocr &= ~VDD_165_195;}current_ocr = mmcsd_select_voltage(host, ocr); /* 配置SDIO外设设置为合适的电压,对于stm32、gd32等相关控制器,实际是不支持不同等级电压配置的,所以这里可以忽略,不过你需要注意你所使用的sd卡的电源在硬件上是匹配的 *//** Can we support the voltage(s) of the card(s)?*/if (!current_ocr){err = -RT_ERROR;goto err;}/** Detect and init the card.*/err = mmcsd_sd_init_card(host, current_ocr); /* 完整的SD卡初始化流程在此函数内实现 */if (err)goto err;mmcsd_host_unlock(host); /* 释放锁 */err = rt_mmcsd_blk_probe(host->card); /* 注册块设备 */if (err) /* 如果注册块设备失败,将移除卡 */goto remove_card;mmcsd_host_lock(host); /* 获取锁 */return 0;remove_card:mmcsd_host_lock(host); /* 获取锁 */rt_mmcsd_blk_remove(host->card); /* 移除块设备 */rt_free(host->card); /* 释放对应的内存 */host->card = RT_NULL;
err:LOG_D("init SD card failed!");return err;
}
- 调用
mmcsd_sd_init_card()
函数完成SD卡检测以及初始化配置
static rt_int32_t mmcsd_sd_init_card(struct rt_mmcsd_host *host,rt_uint32_t ocr)
{struct rt_mmcsd_card *card;rt_int32_t err;rt_uint32_t resp[4];rt_uint32_t max_data_rate;mmcsd_go_idle(host); /* 发送CMD0,复位SD卡,使卡进入空闲模式 *//** If SD_SEND_IF_COND indicates an SD 2.0* compliant card and we should set bit 30* of the ocr to indicate that we can handle* block-addressed SDHC cards.*/err = mmcsd_send_if_cond(host, ocr); /* 发送CMD8指令,判断是否为V2.0或V2.0以上的卡,并获取OCR寄存器值 */if (!err) /* 如果是V2.0及以上版本的卡,将置为OCR的bit30位,表明主机支持高容量SDHC卡(OCR将在ACMD41指令时作为参数发送给卡) */ocr |= 1 << 30;err = mmcsd_send_app_op_cond(host, ocr, RT_NULL); /* 发送ACMD41(ACMD41 = CMD55+CMD41)指令,发送主机容量支持信息,并询问卡的操作条件 */if (err)goto err;if (controller_is_spi(host)) /* 判断是否使用SPI方式访问SD卡 */err = mmcsd_get_cid(host, resp); /* 采用SPI方式获取CID寄存器值 */elseerr = mmcsd_all_get_cid(host, resp);/* 发送CMD2命令,获取CID寄存器值 */if (err)goto err;card = rt_malloc(sizeof(struct rt_mmcsd_card)); /* 创建rt_mmcsd_card结构体,用于存储对应SD卡的CID寄存器内容 */if (!card){LOG_E("malloc card failed!");err = -RT_ENOMEM;goto err;}rt_memset(card, 0, sizeof(struct rt_mmcsd_card));card->card_type = CARD_TYPE_SD;card->host = host;rt_memcpy(card->resp_cid, resp, sizeof(card->resp_cid));/** For native busses: get card RCA and quit open drain mode.*/if (!controller_is_spi(host)) /* 如果不是采用SPI方式访问SD卡 */{err = mmcsd_get_card_addr(host, &card->rca); /* 发送CMD3命令,获取RCA地址 */if (err)goto err1;mmcsd_set_bus_mode(host, MMCSD_BUSMODE_PUSHPULL);/* 设置CMD总线为推挽输出模式,需要注意的是,MMC卡V3.31版本以前的卡,初始化阶段,CMD总线需要为开路模式,对于SD/SD I/O卡和MMC V4.2在初始化时也使用推挽驱动 */}err = mmcsd_get_csd(card, card->resp_csd); /* 发送CMD9命令,获取CSD寄存器值 */if (err)goto err1;err = mmcsd_parse_csd(card); /* 解析CSD寄存器值,将解析完成的数据存放在刚刚申请的card结构体内 */if (err)goto err1;if (!controller_is_spi(host)) /* 如果不是采用SPI方式访问SD卡 */{err = mmcsd_select_card(card); /* 发送CMD7命令,选择卡 */if (err)goto err1;}err = mmcsd_get_scr(card, card->resp_scr); /* 发送CMD9命令,获取SCR寄存器值,并保存在刚刚申请的card结构体内 */if (err)goto err1;mmcsd_parse_scr(card); /* 解析SCR寄存器的值,并将解析结果存放在在card结构体内 */if (controller_is_spi(host)){err = mmcsd_spi_use_crc(host, 1);if (err)goto err1;}/** change SD card to high-speed, only SD2.0 spec*/err = mmcsd_switch(card); /* 发送CMD6指令,切换卡访问速率由默认的12.5MB/Sec为25MB/Sec高速接口 */if (err)goto err1;/* set bus speed */max_data_rate = (unsigned int)-1;if (card->flags & CARD_FLAG_HIGHSPEED){if (max_data_rate > card->hs_max_data_rate)max_data_rate = card->hs_max_data_rate;}else if (max_data_rate > card->max_data_rate){max_data_rate = card->max_data_rate;}mmcsd_set_clock(host, max_data_rate); /* 修改SDIO外设时钟速度 *//*switch bus width*/if ((host->flags & MMCSD_BUSWIDTH_4) &&(card->scr.sd_bus_widths & SD_SCR_BUS_WIDTH_4)) /* 根据SD卡的SCR寄存器反馈的值,判断SD卡是否支持4线宽度访问模式,如果支持则切换为4线宽度访问模式 */{err = mmcsd_app_set_bus_width(card, MMCSD_BUS_WIDTH_4); /* 发送ACMD6(ACMD6=CMD55+CMD6)指令,通知SD卡切换为4线访问模式 */if (err)goto err1;mmcsd_set_bus_width(host, MMCSD_BUS_WIDTH_4); /* 修改SDIO外设配置为4线访问模式 */}host->card = card; /* 将card结构体数据与host结构体建立绑定关系 */return 0;err1:rt_free(card);
err:return err;
}
6. 调试记录
RT-Thread的SDIO驱动,默认上层使用到了 elm-fatfs
文件系统,因此通常我们配置好对应的芯片的SDIO驱动之后,直接就可以快速使用文件系统来操作访问SD Nand了,关于文件系统的有关内容,不在此文中做过多描述,有兴趣的同学可以关注本人博客,后续将及时更新。
此外,在实际使用中有一点需要注意,当我们首次使用芯片的时候,sd nand内还未写入任何数据,此时通常是没有文件系统的,所以当一次执行之后你会见到如下错误:
这是由于SD nand内没有挂载文件系统导致,解决此问题有以下两个方法:
方法一:在命令终端使用
mkfs
挂载文件系统,具体命令步骤如下:- 使用list_device查看sd nand对应的设备名
- 使用
mkfs
命令格式化sd nand:mkfs -t elm sd0
(-t
指定文件系统类型为elm-FAT文件系统,对sd0设备操作)
- 使用list_device查看sd nand对应的设备名
方法二:将SD nand通过读卡器,插入电脑,在电脑上进行格式化U盘操作,不过此操作需要SD nand的转接板
7. 总结
以上便是SD卡的识别与初始化流程,整体流程简单的梳理一下,大致如下:
由
drv_sdio.c
外设驱动或其他调用mmcsd_change()
触发mmcsd_detect()
检测在
mmcsd_detect ()
任务中,实现对SD卡、SD I/O卡、MMC卡的初步识别(发送对应卡特有命令,并判断是否正确响应),之后根据卡片类型调用不同类型卡片驱动文件内的初始化程序如针对SD卡,则调用
sd.c
文件内的init_sd()
函数完成在
init_sd()
函数内调用mmcsd_sd_init_card()
完成SD卡的完整识别流程以及初始化流程,同时同步修改SDIO外设配置SD卡初始化完成之后,调用
rt_mmcsd_blk_probe()
将sd卡注册为块设备至此SD的识别与初始化流程顺利完成
相关文章推荐:
- (点击跳转)SD Nand 与 SD卡 SDIO模式应用流程
- (点击跳转)SD nand与SD卡 SPI模式驱动
创作不易,转载请注明出处!
关注、点赞+收藏,可快速查看后续分享哦!
rt-thread SDIO驱动框架分析(SD卡驱动\SD Nand驱动)相关推荐
- I2C驱动框架分析(3):DW_I2C驱动分析
I2C驱动框架分析(1):I2C重要概念与数据结构 I2C驱动框架分析(2):I2C框架源码分析 I2C驱动框架分析(3):DW_I2C驱动分析 第三章:DW_I2C驱动 其驱动文件在drivers/ ...
- S3C2440 Nand Flash驱动(分析MTD层并制作NAND驱动)(二十三)
http://www.cnblogs.com/lifexy/p/7701181.html 1.本节使用的nand flash型号为K9F2G08U0C,它的命令如下: 1.1 我们以上图的Read I ...
- Linux i2c驱动框架分析 (二)
Linux i2c驱动框架分析 (一) Linux i2c驱动框架分析 (二) Linux i2c驱动框架分析 (三) 通用i2c设备驱动分析 i2c core i2c核心(drivers/i2c/i ...
- SD nand 与 SD卡的SPI模式驱动
文章目录 SD nand 与 SD卡的SPI模式驱动 1. 概述 2. SPI接口模式与SD接口模式区别 2.1 接口模式区别 2.2 硬件引脚 2.3 注意事项 3. SD接口协议 3.1 命令 3 ...
- Linux PCI驱动框架分析:(Peripheral Component Interconnect,外部设备互联)
<DPDK 20.05 | rte_pci_bus思维导图 | 第一版> <linux系统下:IO端口,内存,PCI总线 的 读写(I/O)操作> <Linux指令:ls ...
- SDIO协议读写SD卡之SD卡简介
SD卡的用途 SD卡是一种主要以Nand Flash作为存储介质,具有体积小.数据传输速度快以及支持热插拔的优点.如今,已被广泛应用于数码相机.便携式移动设备以及手机等多种设备中. SD卡分类 (一) ...
- Linux USB驱动框架分析 【转】
转自:http://blog.chinaunix.net/uid-11848011-id-96188.html 初次接触与OS相关的设备驱动编写,感觉还挺有意思的,为了不至于忘掉看过的东西,笔记跟总结 ...
- framebuffer驱动详解2——framebuffer驱动框架分析
以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除. 一.framebuffer驱动框架总览 1.驱动框架部分 (1)drivers/video/fbmem.c(主要的文件) 创建graphic ...
- LCD 设备驱动框架分析及核心结构
Linux 下很多东西都是和结构体相关,举个例子,时钟大家都知道吧,Linux 下对应时钟的东西就有好几个结构体,所以你要是想明白Linux 下那些东西,对结构体要有所了解,LCD 是基础的驱动设备, ...
最新文章
- 爬虫之requests模块发送带header的请求
- atomikos mysql,记一次 Atomikos 分布式事务的使用
- easyui使用时出现这个Uncaught TypeError: Cannot read property 'combo' of undefined
- (批量)备份github仓库到本地
- PHP中array_merge函数与array+array的区别
- python 编译器pyc_有没有办法知道哪个Python版本.pyc文件被编译?
- js ajax 访问java函数,将url分配给Java EE项目中的ajax(js)函数
- Linux获取外部程序指针,Linux内核获取当前进程指针
- linux系统date命令(时间戳与日期相互转换)
- Linux系统下如何复制粘贴文件(待更新)
- android setting 开发者模式,Android 设置 Setting ---开发者选项 中选项为默认配置
- 流体仿真前处理,Fluent工程师的”基本功”和”看家本领”
- c51语言转换ASCII码,数据转换:C51数字转字符最快速的方法 ASCII转换
- checkbox列表选择2
- 网络安全2020.6.15作业
- 手把手教你解决宏基笔记本wifi开关故障(超详细)
- 39-网上商城数据库-用户信息数据操作
- 百度搜索广告投放的展现位置!百度推广广告是如何扣费的?
- 2022.2.14-2.20 AI行业周刊(第85期):失业
- Python3:pygame模块的简单的使用(加载图片并让图片动起来)
热门文章
- Deepin打开或者关闭触控板
- SGM58031示例
- [golang]-通过node_exporter监控GPU以及cpu频率、温度
- Java使用XDOC实现word,pdf等格式的生成及转换
- 江苏对口单招计算机技能历年真题,2012江苏省对口单招计算机技能试卷
- 解除安卓车机禁止安装软件_赫兹app下载安装-赫兹交友软件下载v3.5.7 安卓官方版...
- phpcms v9网站搬家(详细步骤)完全教程
- linux 登录服务失败,向日葵linux版总是提示登录失败,连接不上服务器怎么班?...
- js打印复选框不勾选
- 做自媒体工具和软件有哪些?整理8类工具分享给大家!