stm32f103zet6移植标准库的sdio驱动

2023-11-14

sdio移植

st官网给的标准库有给一个用于st出的评估板的sdio外设实现,但一是文件结构有点复杂,二是相比于国内正点原子和野火的板子也有点不同,因此还是需要移植下才能使用。当然也可以直接使用正点原子或野火提供的实例,但为了熟悉下sdio的一些知识,本人还是决定从st标准库移植一次。下面就是移植的过程。

硬件差异

st官方提供的10E评估板和本人使用的野火stm32f103zet6开发板可以兼容。但在TF卡插入检测有所不同。st官方的stm3210e评估板使用PF11用于卡插入检测,而stm32f103zet6开发板没有提供此检测引脚功能。

移植内容

  1. 复制STM32F10x_StdPeriph_Lib_V3.5.0/Utilities/STM32_EVAL/Common/stm32_eval_sdio_sd.cSTM32F10x_StdPeriph_Lib_V3.5.0/Utilities/STM32_EVAL/Common/stm32_eval_sdio_sd.h到自己的项目中
  2. 自己项目中新建一个sdio_gpio.csdio_gpio.h。复制STM32F10x_StdPeriph_Lib_V3.5.0/Utilities/STM32_EVAL/STM3210E_EVAL/stm3210e_eval.c中的几个函数实现到sdio_gpio.c文件中。这里就列出sdio_gpio.h头文件,具体函数就自己复制了。
#ifndef __SDIO_GPIO_H
#define __SDIO_GPIO_H

#include "stm32f10x_conf.h"

// 这里的3个宏是驱动中会使用到的,都是官方实例中复制出来的
#define SDIO_FIFO_ADDRESS                ((uint32_t)0x40018080)
/**
  * @brief  SDIO Intialization Frequency (400KHz max)
  */
#define SDIO_INIT_CLK_DIV                ((uint8_t)0xB2)
/**
  * @brief  SDIO Data Transfer Frequency (25MHz max)
  */
#define SDIO_TRANSFER_CLK_DIV            ((uint8_t)0x00)


// 头文件中只是这几个函数的声明,函数的具体内容要复制到sd_gpio.c文件中
void SD_LowLevel_DeInit(void);
void SD_LowLevel_Init(void);
void SD_LowLevel_DMA_TxConfig(uint32_t *BufferSRC, uint32_t BufferSize);
void SD_LowLevel_DMA_RxConfig(uint32_t *BufferDST, uint32_t BufferSize);
uint32_t SD_DMAEndOfTransferStatus(void);
// 这里中断初始化改了一个名字,防止和别的外设中断初始化重名
void sdio_NVIC_Configuration(void)

#endif
  1. 复制好函数体后,删除sdio_gpio.c文件中出现"DETECT"的地方,原因就是因为官方评估板中有DETECT引脚,而本人板子实际没有DETECT引脚
  2. 更改一些stm32_eval_sdio_sd.c文件中的内容
static SD_Error CmdResp2Error(void);
//这里注释了,因为在sd_gpio.c中定义了,不再是私有函数
//static uint32_t SD_DMAEndOfTransferStatus(void);
static SD_Error CmdResp6Error(uint8_t cmd, uint16_t *prca);

// SD_Init()函数中加入中断初始化
SD_Error SD_Init(void)
{
  SD_Error errorstatus = SD_OK;

  /* SDIO Peripheral gpio 初始化 */
  SD_LowLevel_Init();

  SDIO_DeInit();

  // 中断初始化
  sdio_NVIC_Configuration();

  errorstatus = SD_PowerON();
 ...
}

// SD_GetState()删除detect内容
/**
  * @brief  Returns the current card's state.
  * @brief  因为采用data3信号线作为插入检测,因此此处不能使用SD_Detect()函数
  * @param  None
  * @retval SDCardState: SD Card Error or SD Card Current State.
  */
SDCardState SD_GetState(void)
{
  uint32_t resp1 = 0;

  /*if(SD_Detect()== SD_PRESENT)*/
  /*{*/
    if (SD_SendStatus(&resp1) != SD_OK)
    {
      return SD_CARD_ERROR;
    }
    else
    {
      return (SDCardState)((resp1 >> 9) & 0x0F);
    }
  /*}*/
  /*else*/
  /*{*/
    /*return SD_CARD_ERROR;*/
  /*}*/
}

// SD_Detect()函数全部注释了
/**
 * @brief  Detect if SD card is correctly plugged in the memory slot.
 * @brief  因为没有单独的插入检测,因此不使用此函数
 * @param  None
 * @retval Return if SD is detected or not
 */
/*uint8_t SD_Detect(void)*/
/*{*/
  /*__IO uint8_t status = SD_PRESENT;*/

  /*[>!< Check GPIO to detect SD <]*/
  /*if (GPIO_ReadInputDataBit(SD_DETECT_GPIO_PORT, SD_DETECT_PIN) != Bit_RESET)*/
  /*{*/
    /*status = SD_NOT_PRESENT;*/
  /*}*/
  /*return status;*/
/*}*/

//  SD_WriteMultiBlocks()中加入一个命令,书中说加入有用,具体不清楚
SD_Error SD_WriteMultiBlocks(uint8_t *writebuff, uint32_t WriteAddr, uint16_t BlockSize, uint32_t NumberOfBlocks)
{
  SD_Error errorstatus = SD_OK;
  __IO uint32_t count = 0;

  TransferError = SD_OK;
  TransferEnd = 0;
  StopCondition = 1;

  SDIO->DCTRL = 0x0;

  if (CardType == SDIO_HIGH_CAPACITY_SD_CARD)
  {
    BlockSize = 512;
    WriteAddr /= 512;
  }

  /*!< 根据正点原子代码加入,说明是为了防止DMA检测卡死 */
  SDIO_CmdInitStructure.SDIO_Argument = (uint32_t) BlockSize;
  SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_BLOCKLEN;
  SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
  SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
  SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
  SDIO_SendCommand(&SDIO_CmdInitStructure);


  errorstatus = CmdResp1Error(SD_CMD_SET_BLOCKLEN);

  if (errorstatus != SD_OK)
  {
    return(errorstatus);
  }
  
  ...
}
  1. 还有一个sdio的中断函数还要忘记了,放在stm32f10x_it.c或main.c都一样的。
/*
 * 函数名:SDIO_IRQHandler
 * 描述  :在SDIO_ITConfig()这个函数开启了sdio中断 ,
      数据传输结束时产生中断
 * 输入  :无
 * 输出  :无
 */
void SDIO_IRQHandler(void)
{
  /* Process All SDIO Interrupt Sources */
  SD_ProcessIRQSrc();
}
  1. 改好后项目中加入sdio_gpio.hstm32_eval_sdio_sd.h头文件。就可以试着编译下了

加入测试代码

因为本人之前的项目中使用的freertos,且已经调通uart0串口。st官方实例是使用几个LED灯指示测试结果,本人开发板上没有这么多的灯,就直接使用串口的printf显示了,一样的方便。下面就是main.c文件。

#include "stm32f10x_conf.h"

//extern __IO uint16_t ADCConvertedValue;
extern __IO uint16_t ADC_DualConvertedValueTab[4];
extern QueueHandle_t command_buf;

typedef enum {FAILED = 0, PASSED = !FAILED} TestStatus;
#define BLOCK_SIZE            512 /* Block Size in Bytes */

#define NUMBER_OF_BLOCKS      32  /* For Multi Blocks operation (Read/Write) */
#define MULTI_BUFFER_SIZE    (BLOCK_SIZE * NUMBER_OF_BLOCKS)

#define SD_OPERATION_ERASE          0
#define SD_OPERATION_BLOCK          1
#define SD_OPERATION_MULTI_BLOCK    2
#define SD_OPERATION_END            3

uint8_t Buffer_Block_Tx[BLOCK_SIZE], Buffer_Block_Rx[BLOCK_SIZE];
uint8_t Buffer_MultiBlock_Tx[MULTI_BUFFER_SIZE], Buffer_MultiBlock_Rx[MULTI_BUFFER_SIZE];

void SD_EraseTest(void);
void SD_SingleBlockTest(void);
void SD_MultiBlockTest(void);
void Fill_Buffer(uint8_t *pBuffer, uint32_t BufferLength, uint32_t Offset);
TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint32_t BufferLength);
TestStatus eBuffercmp(uint8_t* pBuffer, uint32_t BufferLength);
SD_Error Status = SD_OK;
volatile TestStatus EraseStatus = FAILED, TransferStatus1 = FAILED, TransferStatus2 = FAILED;
__IO uint32_t SDCardOperation = SD_OPERATION_ERASE;

void sdio_test_task(void *pvParameter)
{
  if((Status = SD_Init()) != SD_OK)
    printf("sdio init error");
  while(Status == SD_OK && SDCardOperation != SD_OPERATION_END){
    switch(SDCardOperation){
      case SD_OPERATION_ERASE:
        SD_EraseTest();
        SDCardOperation = SD_OPERATION_BLOCK;
        break;
      case SD_OPERATION_BLOCK:
        SD_SingleBlockTest();
        SDCardOperation = SD_OPERATION_MULTI_BLOCK;
        break;
      case SD_OPERATION_MULTI_BLOCK:
        SD_MultiBlockTest();
        SDCardOperation = SD_OPERATION_END;
        break;
    }
  }

  while(1){
  }
}

void dac_task(void *pvParameter){
  static unsigned short num = 0xffff;

  for(;;){
    DAC_SetChannel1Data(DAC_Align_12b_L, num);
		DAC_SoftwareTriggerCmd(DAC_Channel_1, ENABLE);
    num -= 0x2;
    if(num < 0xff)
      num = 0xffff;
		vTaskDelay(2);
  }
}

void system_init(void *pvParameter){
  SystemInit();
  dac_init();
  serial_init();
	//single_adc_init();
  muiltichannel_adc_init();
	command_buf = xQueueCreate(100, sizeof(struct command_t));
  vTaskDelete(NULL);
}

void print_test_task(void *pvParameter){
  for(;;){
    printf("This just test for uart\r\n");
    vTaskDelay(500);
  }
}

//void adc_task(void *pvParameter){
//	// 软件开启ADC1转换
//  ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//  for(;;){
//    printf("Current ADC voltage is %f\r\n", ADCConvertedValue * 3.3/4096);
//    vTaskDelay(5);
//  }
//}

void muiltichannel_adc_task(void *pvParameter){
  muiltichannel_adc_enable();
  for(;;){
    printf("PA1 voltage is %.2f\r\n", ADC_DualConvertedValueTab[0] * 3.3 / 4096);
    printf("PA2 voltage is %.2f\r\n", ADC_DualConvertedValueTab[1] * 3.3 / 4096);
    printf("PA3 voltage is %.2f\r\n", ADC_DualConvertedValueTab[2] * 3.3 / 4096);
    printf("PA5 voltage is %.2f\r\n", ADC_DualConvertedValueTab[3] * 3.3 / 4096);
    vTaskDelay(2);
  }
}


int main(void)
{
	xTaskCreate(system_init, "system init task", 500, NULL, 2, NULL);
  //xTaskCreate(print_test_task, "uart print test", 200, NULL, 1, NULL);
  /*xTaskCreate(dac_task, "dac task for led breath", 1000, NULL, 1, NULL);*/
  //xTaskCreate(adc_task, "adc test task", 1000, NULL, 1, NULL);
 /*xTaskCreate(muiltichannel_adc_task, "mNuiltichannel adc test task", 1000, NULL, 1, NULL);*/
  xTaskCreate(sdio_test_task, "sdio test", 200, NULL, 1, NULL);

	vTaskStartScheduler();
}

#ifdef  USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line)
{
  while (1)
  {
    printf("Assert error in %s, line %d\r\n",file, line);
  }
}
#endif

void vApplicationStackOverflowHook( TaskHandle_t xTask,char * pcTaskName ){
  while (1)
  {
    printf("Malloc error for %s\r\n",pcTaskName);
  }
}

void SD_EraseTest(void)
{
  if (Status == SD_OK)
  {
    /* Erase NumberOfBlocks Blocks of WRITE_BL_LEN(512 Bytes) */
    Status = SD_Erase(0x00, (BLOCK_SIZE * NUMBER_OF_BLOCKS));
  }

  if (Status == SD_OK)
  {
    Status = SD_ReadMultiBlocks(Buffer_MultiBlock_Rx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);

    /* Check if the Transfer is finished */
    Status = SD_WaitReadOperation();

    /* Wait until end of DMA transfer */
    while(SD_GetStatus() != SD_TRANSFER_OK);
  }

  /* Check the correctness of erased blocks */
  if (Status == SD_OK)
  {
    EraseStatus = eBuffercmp(Buffer_MultiBlock_Rx, MULTI_BUFFER_SIZE);
  }
  if(EraseStatus == PASSED)
  {
    printf("sdio erase test passed\r\n");
  }
  else
  {
    printf("sdio erase test failed\r\n");
  }
}


void SD_SingleBlockTest(void)
{
  /* Fill the buffer to send */
  Fill_Buffer(Buffer_Block_Tx, BLOCK_SIZE, 0x320F);

  if (Status == SD_OK)
  {
    /* Write block of 512 bytes on address 0 */
    Status = SD_WriteBlock(Buffer_Block_Tx, 0x00, BLOCK_SIZE);
    /* Check if the Transfer is finished */
    Status = SD_WaitWriteOperation();
    while(SD_GetStatus() != SD_TRANSFER_OK);
  }

  if (Status == SD_OK)
  {
    /* Read block of 512 bytes from address 0 */
    Status = SD_ReadBlock(Buffer_Block_Rx, 0x00, BLOCK_SIZE);
    /* Check if the Transfer is finished */
    Status = SD_WaitReadOperation();
    while(SD_GetStatus() != SD_TRANSFER_OK);
  }

  /* Check the correctness of written data */
  if (Status == SD_OK)
  {
    TransferStatus1 = Buffercmp(Buffer_Block_Tx, Buffer_Block_Rx, BLOCK_SIZE);
  }

  if(TransferStatus1 == PASSED)
  {
    printf("sdio single block test passed\r\n");
  }
  else
  {
    printf("sdio single block test failed\r\n");
  }
}

void SD_MultiBlockTest(void)
{
  /*--------------- Multiple Block Read/Write ---------------------*/
  /* Fill the buffer to send */
  Fill_Buffer(Buffer_MultiBlock_Tx, MULTI_BUFFER_SIZE, 0x0);

  if (Status == SD_OK)
  {
    /* Write multiple block of many bytes on address 0 */
    Status = SD_WriteMultiBlocks(Buffer_MultiBlock_Tx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
    /* Check if the Transfer is finished */
    Status = SD_WaitWriteOperation();
    while(SD_GetStatus() != SD_TRANSFER_OK);
  }

  if (Status == SD_OK)
  {
    /* Read block of many bytes from address 0 */
    Status = SD_ReadMultiBlocks(Buffer_MultiBlock_Rx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
    /* Check if the Transfer is finished */
    Status = SD_WaitReadOperation();
    while(SD_GetStatus() != SD_TRANSFER_OK);
  }

  /* Check the correctness of written data */
  if (Status == SD_OK)
  {
    TransferStatus2 = Buffercmp(Buffer_MultiBlock_Tx, Buffer_MultiBlock_Rx, MULTI_BUFFER_SIZE);
  }

  if(TransferStatus2 == PASSED)
  {
    printf("sdio muilti block test passed\r\n");
  }
  else
  {
    printf("sdio muilti block test error\r\n");
  }
}


TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint32_t BufferLength)
{
  while (BufferLength--)
  {
    if (*pBuffer1 != *pBuffer2)
    {
      return FAILED;
    }

    pBuffer1++;
    pBuffer2++;
  }

  return PASSED;
}

void Fill_Buffer(uint8_t *pBuffer, uint32_t BufferLength, uint32_t Offset)
{
  uint16_t index = 0;

  /* Put in global buffer same values */
  for (index = 0; index < BufferLength; index++)
  {
    pBuffer[index] = index + Offset;
  }
}

TestStatus eBuffercmp(uint8_t* pBuffer, uint32_t BufferLength)
{
  while (BufferLength--)
  {
    /* In some SD Cards the erased state is 0xFF, in others it's 0x00 */
    if ((*pBuffer != 0xFF) && (*pBuffer != 0x00))
    {
      return FAILED;
    }

    pBuffer++;
  }

  return PASSED;
}

以上的大部分内容都是复制的STM32F10x_StdPeriph_Lib_V3.5.0/Project/STM32F10x_StdPeriph_Examples/SDIO/uSDCard/main.c的内容。只是在此基础上加入了freertos的格式,将测试最后通过和出错的提示方式全部使用串口打印出来。
下面是串口显示的打印结果:

sdio erase test passed
sdio single block test passed
sdio muilti block test passed

至此sdio基本驱动移植成功,可以移植fatfs了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

stm32f103zet6移植标准库的sdio驱动 的相关文章

  • java中byte和Byte详解

    写程序时 误把byte写作Byte 调试了许久 便将二者的区别及用法详细理解一遍 1 byte标题和Byte详解 byte是java的基本数据类型 存储整型数据 占据1个字节 8 bits 能够存储的数据范围是 128 127 Byte是j
  • Windows环境下ARM集成开发环境的搭建与使用

    Windows环境下能够使用Eclipse IDE for C C Developers来搭建ARM开发环境 本文地址 http blog csdn net u011833609 article details 30290655 安装过程例
  • 继续教育自动听课软件_2017继续教育挂机软件下载

    继续教育自动挂机软件2017是一款帮助用户挂机学习的辅助工具 有了它你就不用一直呆在电脑前了 它可以自动更新学习时间 自动换课计时 安全无病毒 方便快捷 省时省力省心 全国中小教师继续教育网的各种远程培训学习 全国各省份都能使用 使用教程
  • Valid注解在嵌套对象中校验失效

    接收对象声明 import lombok Data import javax validation constraints NotBlank import javax validation constraints NotNull impor

随机推荐

  • 数学的幽默打油诗

    1 常微分学常没分 数理方程没天理 实变函数学十遍 泛函分析心犯寒 微分拓扑躲不脱 随机过程随机过 微机原理闹危机 汇编语言不会编 量子力学量力学 机械制图机械制 2 高数 拉格朗日 傅立叶旁 我凝视你凹函数般的脸庞 微分了忧伤 积分了希望
  • 宁波到西塘可以坐火车去吗?

    宁波 嘉善 西塘宁波 嘉善火车 N406 N407 空调快速宁波15 10出发嘉善19 27到达4小时17分270公里硬座 42元硬卧 93元然后从嘉善打车到西塘 9公里 15元左右 坐快客3元也可以乘坐T794 空调特快宁波 10 41出
  • python 实现自动批量下载腾讯在线excel

    python 批量下载腾讯在线文档 如需要源代码供参考 可以留言邮箱 看到的话就发一下 pthon自动批量下载腾讯在线文档 对于大量实时更新维护的在线文档 可以随时轻松自动化批量下载在线文档 无需耗费人工下载 腾讯在线文档标签有时候会随版本
  • BP神经网络算法基本原理,BP神经网络算法流程图

    伤寒 副伤寒流行预测模型 BP神经网络 的建立 由于目前研究的各种数学模型或多或少存在使用条件的局限性 或使用方法的复杂性等问题 预测效果均不十分理想 距离实际应用仍有较大差距 NNT是Matlab中较为重要的一个工具箱 在实际应用中 BP
  • 爬虫中有关验证码的问题处理

    在爬虫中 经常要处理登陆的相关事宜 有时候登陆界面会需要提交验证码 如何处理验证码 解决办法 若是自己编写模块 需要涉及深度学习 这就是另一块大的内容了 在这里简单调用已经封装好的模块来实现获取验证码 本文以超级鹰为例 网址 http ww
  • PADS 原理图如何自动编号

    PADS原理图如何自动编号 PADS 原理图工具 PowerLogic exe 不支持元件位号重名 原生不提供自动编号功能 虽然 PowerPCB exe 可以支持元件位号自动编号功能再同步到原理图 但是其局限性太大没啥实际意义 另外 PA
  • YUV格式学习:YUYV、YVYU、UYVY、VYUY格式转换成RGB

    YUYV YVYU UYVY VYUY格式 它们都是YUV422的打包格式 即在内存中 Y U V都是挨着排序的 它们的名称就表示了Y U V的顺序 像YUYV 就是Y U Y V Y U Y V 在做转换时 就显得很容易 简单了 因为极其
  • 【卷积核设计】10、Scaling Up Your Kernels to 31x31

    文章目录 一 背景 二 方法 三 RepKLNet a Large Kernel Architecture 3 1 结构 3 2 尽可能的让卷积核变大 3 3 图像分类 3 4 语义分割 3 5 目标检测 四 分析 五 限制 六 结论 论文
  • 【笔试强训选择题】Day43.习题(错题)解析

    作者简介 大家好 我是未央 博客首页 未央 303 系列专栏 笔试强训选择题 每日一句 人的一生 可以有所作为的时机只有一次 那就是现在 文章目录 前言 一 Day43习题 错题 解析 总结 前言 一 Day43习题 错题 解析 1 解析
  • Elasticsearch笔记4 基础入门

    执行分布式检索 一个查询操作 在ES分布式环境中分为两步 查询与合并 查询阶段 ES集群向所有分片传递查询语句 分片接收到请求后 执行搜索并建立一个长度为top n的优先队列 存储结果 top n 的大小取决于分页参数 top n from
  • 链表c语言stl,C++STL之List容器

    1 再谈链表 List链表的概念再度出现了 作为线性表的一员 C 的STL提供了快速进行构建的方法 为此 在前文的基础上通过STL进行直接使用 这对于程序设计中快速构建原型是相当有必要的 这里的STL链表是单链表的形式 2 头文件 头文件
  • Vue3+Vite项目配置Eslint+Prettier+Husky+Lint-Staged+Commitlint

    Eslint 配置 ESLint 是一个插件化并且可配置的 JavaScript 语法规则和代码风格的检查工具 ESLint 能够帮你轻松写出高质量的 JavaScript 代码 1 建议 vscode 安装 Eslint 的插件 这个插件
  • 用JAVASCRIPT从弹出的窗口中获取值

    设三个页面 Father aspx SubOpen aspx SubModalDialog aspx 在Father aspx中
  • Excel构建决策分析模型

    特点 探讨使用 Excel 构建决策模型的价值和重要性 以及对 Excel 复杂性的非常详细和深入的解释 使用 Excel 的图形功能来有效地呈现定量数据 比率和间隔 来通知和影响目标对象 利用 Excel 的内置数据可视化和操作功能准备数
  • kubernetes session保持等设置

    session保持 如何在service内部实现session保持呢 当然是在service的yaml里进行设置啦 在service的yaml的sepc里加入以下代码 sessionAffinity ClientIP sessionAffi
  • matplotlib怎么在一张图上画多条曲线?

    问题 多个plot画不到一张图上 解决方法 多个plot用一个plt show 即可 一次plt show 就会有一次输出 如何让函数画在同一张画布上 for i in range 1 15 3 train score test score
  • bh1750c语言程序,BH1750FVI数字光线强度传感器 51单片机源程序

    BH1750FVI IIC测试程序 使用单片机STC89C51 晶振 11 0592M 显示 LCD1602 编译环境 Keil uVision2 参考宏晶网站24c04通信程序 时间 2011年4月20日 include include
  • vue3表格导出excel

    下载依赖 npm install xlsx 引入依赖 import as XLSX from xlsx 使用
  • Python内置类型转换函数

    chr i chr 函数返回ASCII码对应的字符串 gt gt gt print chr 65 A gt gt gt print chr 66 B gt gt gt print chr 65 chr 66 AB complex real
  • stm32f103zet6移植标准库的sdio驱动

    sdio移植 st官网给的标准库有给一个用于st出的评估板的sdio外设实现 但一是文件结构有点复杂 二是相比于国内正点原子和野火的板子也有点不同 因此还是需要移植下才能使用 当然也可以直接使用正点原子或野火提供的实例 但为了熟悉下sdio