[嵌入式TDD实战]TDD开发AT26DF驱动模块(五):模块写完

前言

在上一章中,我们写出了第二个接口并在测试用例的保障下进行了重构。接下来的事就很程序化了。查看下一个操作的文档,写测试,实现接口。

流水账

ProgramInPage

分页内编程的接口,操作符后3字节地址,然后一直传要写入的字节。

别看文档上介绍了一堆机制,其实咱在写这个接口时并不关心,因为这是很底层的接口,只提供操作的抽象,具体怎么应用这些机制咱不负责。唯一需要关心下的是256字节的上限,如果超过256字节时会有一些异常啥的话可能我们需要检查下参数。但是阅读文档发现超过了并没有关系,只是会在芯片内部缓冲区覆盖前面写入的。那就无所谓了。

没什么好说的,测试,实现,完成。。。

AT26DF.h

// description: allows anywhere from a single byte of data to 256 bytes of data to be programmed
//              into previously erased memory locations. 
// parameter  : addr   the starting address
//              buf    data buffer.
//              len    the length of data you want to write.
// return     : 
// note       : if data goes beyond the end of the page will wrap around back to the beginning 
//              of the same page. So, only the last 256 bytes(size of a page) will be programmed
//              into the page specified by addr.
void AT26DF_programInPage(AT26DF_ADDR addr, const uint8_t *buf, uint16_t len);

AT26DFTest.c

TEST(AT26DF, 0LenOrBufNULLDoNothing){
  ……
  AT26DF_programInPage(anyAddr,buf,0);
  AT26DF_programInPage(anyAddr,NULL,1);
}

TEST(AT26DF, programInPage){
  AT26DF_ADDR startAddr = 0x3434;
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_PROGRAM);
  _WriteAddrExpect(startAddr);
  for(i = 0; i < len; i++)
    SPI_writeByte_Expect(expectData[i]);
  SPI_DeselectCS_Expect();

  AT26DF_programInPage(startAddr,expectData,len);
}

AT26DF.c

void AT26DF_programInPage(AT26DF_ADDR addr, const uint8_t *buf, uint16_t len){
  if(buf == NULL || len == 0)
    return;
  _csSel();
  _spiWrite(AT26DF_OPC_PROGRAM);
  _writeAddr(addr);
  _spiBurstWrite(buf,len);
  _csDesel();
}

BlockErase


擦除扇区,有点意思,有三种擦除模式,因此我们的接口可能得给三个。当然也可以多加一个参数来指定是哪一个,但我就是觉得给三个比较好。这个操作很简单,就是操作码加地址。

AT26DF.h

……
#define AT26DF_OPC_BLOCKERASE_4KB     0x20
#define AT26DF_OPC_BLOCKERASE_32KB    0x52
#define AT26DF_OPC_BLOCKERASE_64KB    0xD8
……
// description: Erase a block of 4K-, 32K-, or 64K-bytes
// parameter  : addr   any address in the block to be erased.
// return     : 
// note       : Before a Block Erase command can be started, the WriteEnable command must have
//              been previously issued to the device to set the WEL bit of the Status Register 
//              to a logical “1” state.
//              for a 4K-byte erase, address bits A11-A0 will be ignored by the device and their
//              values can be either a logical “1” or “0”. For a 32K-byte erase, address bits
//              A14-A0 will be ignored, and for a 64K-byte erase, address bits A15-A0 will be
//              ignored by the device.
void AT26DF_blockErase4KB(AT26DF_ADDR addr);
void AT26DF_blockErase32KB(AT26DF_ADDR addr);
void AT26DF_blockErase64KB(AT26DF_ADDR addr);

AT26DFTest.c

……
TEST(AT26DF, blockErase4KB){
  AT26DF_ADDR startAddr = 0x34dfa;
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_BLOCKERASE_4KB);
  _WriteAddrExpect(startAddr);
  SPI_DeselectCS_Expect();

  AT26DF_blockErase4KB(startAddr);
}

TEST(AT26DF, blockErase32KB){
  AT26DF_ADDR startAddr = 0x34dfa;
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_BLOCKERASE_32KB);
  _WriteAddrExpect(startAddr);
  SPI_DeselectCS_Expect();

  AT26DF_blockErase32KB(startAddr);
}

TEST(AT26DF, blockErase64KB){
  AT26DF_ADDR startAddr = 0x34dfa;
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_BLOCKERASE_64KB);
  _WriteAddrExpect(startAddr);
  SPI_DeselectCS_Expect();

  AT26DF_blockErase64KB(startAddr);
}

AT26DF.c

……
void AT26DF_blockErase4KB(AT26DF_ADDR addr){
  _csSel();
  _spiWrite(AT26DF_OPC_BLOCKERASE_4KB);
  _writeAddr(addr);
  _csDesel();
}

void AT26DF_blockErase32KB(AT26DF_ADDR addr){
  _csSel();
  _spiWrite(AT26DF_OPC_BLOCKERASE_32KB);
  _writeAddr(addr);
  _csDesel();
}

void AT26DF_blockErase64KB(AT26DF_ADDR addr){
  _csSel();
  _spiWrite(AT26DF_OPC_BLOCKERASE_64KB);
  _writeAddr(addr);
  _csDesel();
}

哦,明显的冗余。让我们来提取下。

AT26DFTest.c

……
static AT26DF_ADDR startAddr = 0x343dfa;
……
static void _OpWithAddrExpect(uint8_t op){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(op);
  _WriteAddrExpect(startAddr);
  SPI_DeselectCS_Expect();
}
……
TEST(AT26DF, blockErase4KB){
  _OpWithAddrExpect(AT26DF_OPC_BLOCKERASE_4KB);
  AT26DF_blockErase4KB(startAddr);
}

TEST(AT26DF, blockErase32KB){
  _OpWithAddrExpect(AT26DF_OPC_BLOCKERASE_32KB);
  AT26DF_blockErase32KB(startAddr);
}

TEST(AT26DF, blockErase64KB){
  _OpWithAddrExpect(AT26DF_OPC_BLOCKERASE_64KB);
  AT26DF_blockErase64KB(startAddr);
}

AT26DF.c

……
static void _cmdWithAddr(uint8_t op, AT26DF_ADDR addr){
  _csSel();
  _spiWrite(op);
  _writeAddr(addr);
  _csDesel();
}

void AT26DF_blockErase4KB(AT26DF_ADDR addr){
  _cmdWithAddr(AT26DF_OPC_BLOCKERASE_4KB,addr);
}

void AT26DF_blockErase32KB(AT26DF_ADDR addr){
  _cmdWithAddr(AT26DF_OPC_BLOCKERASE_32KB,addr);
}

void AT26DF_blockErase64KB(AT26DF_ADDR addr){
  _cmdWithAddr(AT26DF_OPC_BLOCKERASE_64KB,addr);
}

还有一点小问题,startAddr现在是固定不动的,可能会出问题,那就在TearDown时稍微修改下得了

……
TEST_TEAR_DOWN(AT26DF){
  MockSPI_Verify();
  MockSPI_Destroy();
  startAddr >>= 1;
  if(startAddr == 0)
    startAddr = 0x343dfa;
}
……

大量相似的就直接跳过吧

状态寄存器


大量命令都是类似的。到状态寄存器这算是一个决策点。状态寄存器的不同bit有特定的意义,驱动应该对其作出一定的抽象。一般来说要不就是用结构体位域的方式,要不就是用位掩码的方式。

结构体位域的方式移植性不太好。最终还是决定用位掩码的方式,虽然这样可读性就稍微差了些。于是在头文件中提供了对应的掩码、偏移的定义,这样寄存器就是当做uint8来读写了:

AT26DF.h

……
#define AT26DF_OPC_READSR             0x05   // Read Status Register
#define AT26DF_OPC_WRITESR            0x01   // Write Status Register
……
// mask of Status Register bit fields
#define AT26DF_SRMASK_SPRL   0x80   // mask of Sector Protection Registers Locked bit(1 for locked)
#define AT26DF_SRMASK_RES    0x40   // mask of Reserved for future use bit
#define AT26DF_SRMASK_EPE    0x20   // mask of Erase/Program Error bit(1 for error detected)
#define AT26DF_SRMASK_WPP    0x10   // mask of Write Protect (WP) Pin Status bit(1 for high/deasserted)
#define AT26DF_SRMASK_SWP    0x0C   // mask of Software Protection Status bits(see AT26DF_SWP_XXX)
#define AT26DF_SRMASK_WEL    0x02   // mask of Write Enable Latch Status bit(1 for write enabled)
#define AT26DF_SRMASK_BSY    0x01   // mask of Ready/Busy Status bit(1 for busy)

#define AT26DF_SRMASK_GUP    0x00   // mask to set SR with operation of Global Unprotect(xx0000xx)
#define AT26DF_SRMASK_GP     0x3C   // mask to set SR with operation of Global Protect  (xx1111xx)

// offset of Status Register bit fields
#define AT26DF_SROFST_SPRL   7      // offset of Sector Protection Registers Locked bit
#define AT26DF_SROFST_RES    6      // offset of Reserved for future use bit
#define AT26DF_SROFST_EPE    5      // offset of Erase/Program Error bit
#define AT26DF_SROFST_WPP    4      // offset of Write Protect (WP) Pin Status bit
#define AT26DF_SROFST_SWP    2      // offset of Software Protection Status bits
#define AT26DF_SROFST_WEL    1      // offset of Write Enable Latch Status bit
#define AT26DF_SROFST_BSY    0      // offset of Ready/Busy Status(1 for busy) bit

// value of Software Protection Status

// All sectors are software unprotected (all Sector Protection Registers are 0).
#define AT26DF_SWP_NONE      0
// Some sectors are software protected. Read individual Sector Protection Registers to 
// determine which sectors are protected.
#define AT26DF_SWP_PART      1      
// All sectors are software protected (all Sector Protection Registers are 1 – default).
#define AT26DF_SWP_ALL       3

……
// description: Read the Status Register to determine the device’s ready/busy status, as well as
//              the status of many other functions such as Hardware Locking and Software Protection.
// parameter  : 
// return     : the current value of SR.
// note       : The Status Register can be read at any time, including during an internally
//              self-timed program or erase operation.
uint8_t AT26DF_readSR(void);
// description: To modify the SPRL bit of the Status Register and/or perform a Global Protect or 
//              Global Unprotect operation.
// parameter  : sr    the byte(SPRL bit value, a don't care bit, four data bits to denote whether
//                    a Global Protect or Unprotect should be performed, and two additional don't
//                    care bits) to be written to SR.
// return     : 
// note       : Before the Write Status Register command can be issued, the Write Enable command
//              must have been previously issued to set the WEL bit in the Status Register to a 
//              logical “1”.
//              To perform Global Protect or Global Unprotect operation, with SPRL be 0, writeSR
//              with AT26DF_SRMASK_GUP or AT26DF_SRMASK_GP
void AT26DF_writeSR(uint8_t sr);

AT26DFTest.c

……
TEST(AT26DF, readSR){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READSR);
  SPI_readByte_ExpectAndReturn(0x46);
  SPI_DeselectCS_Expect();

  TEST_ASSERT_EQUAL_HEX8(0x46,AT26DF_readSR());
}

TEST(AT26DF, writeSR){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_WRITESR);
  SPI_writeByte_Expect(0x82);
  SPI_DeselectCS_Expect();

  AT26DF_writeSR(0x82);
}

AT26DF.c

……
uint8_t AT26DF_readSR(void){
  uint8_t ret;
  _csSel();
  _spiWrite(AT26DF_OPC_READSR);
  ret = _spiRead();
  _csDesel();
  return ret;
}

void AT26DF_writeSR(uint8_t sr){
  _csSel();
  _spiWrite(AT26DF_OPC_WRITESR);
  _spiWrite(sr);
  _csDesel();
}

可以想象到肯定会有大量的循环等待SR进入某状态的需求,就像这样:

……
while(sr != xx)
  ;
……

实际上硬件也考虑到了这种情况,所以是可以不取消断言以不断读取SR的值的,所以这个抽象应该由驱动提供。往深了想,这个等待条件可能十分复杂,不好用一个等于或不等来表示,因此干脆就让用户来给出判断函数,判断在某个sr值时是否应该返回,然后返回值中要给出使返回的sr值,因为可能有多种sr值都能导致条件为真,需要让用户知道是哪种状态导致的返回。

// description: Continuously read the Status Register and pass it to the 'cond' callback function
//              till the return value of 'cond' is TRUE.
// parameter  : cond   the call back function to return whether the SR match condition.
// return     : the last value of SR(the one that make cond True).
// note       : 
uint8_t AT26DF_pendSRCondition(BOOL (*cond)(uint8_t sr));

这种情况下就有意思了,内部预期是会在写入操作码后无限制读取,直到满足条件。那我们的测试大概就写成读到几次失败的条件后读到应该成功的状态,然后验证下返回值,于是测试是这样的:

……
static BOOL _SREqualTo3(uint8_t sr){
  return sr == 3;
}

TEST(AT26DF, pendSRCondition){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READSR);
  SPI_readByte_ExpectAndReturn(0x01);
  SPI_readByte_ExpectAndReturn(0x02);
  SPI_readByte_ExpectAndReturn(0x03);
  SPI_DeselectCS_Expect();

  TEST_ASSERT_EQUAL_HEX8(0x03, AT26DF_pendSRCondition(_SREqualTo3));
}

实现是这样的:

……
uint8_t AT26DF_pendSRCondition(BOOL (*cond)(uint8_t sr)){
  uint8_t ret;
  _csSel();
  _spiWrite(AT26DF_OPC_READSR);
  do{
    ret = _spiRead();
  } while (cond(ret) == FALSE);
  _csDesel();
  return ret;
}

然后就又是一堆很无聊的体力劳动了,我就直接略过,给出最终结果吧。

最终文件

最终,经过漫长的过程,模块终于完成了。不要忘了加上各种版权信息和美化下注释啥的。

模块本身早已经发出来了:
https://blog.csdn.net/lin_strong/article/details/90274561

这里补下测试文件:

AT26DFTest.c

#include <string.h>
#include "unity_fixture.h"
#include "AT26DF.h"
#include "MockSPI.h"

TEST_GROUP(AT26DF);
static const uint8_t anyByte = 0xEE;
static const AT26DF_ADDR anyAddr = 0x122534;
static AT26DF_ADDR addrToOperate = 0x343dfa;
static int i;
static uint8_t expectData[] = {0x12, 0x34, 0x56, 0x78, 0x9a};
static uint8_t actualData[sizeof(expectData)];
static const uint8_t len = sizeof(expectData);
static void _WriteAddrExpect(AT26DF_ADDR addr){
  SPI_writeByte_Expect(((addr >> 16) & 0xFF));
  SPI_writeByte_Expect(((addr >> 8) & 0xFF));
  SPI_writeByte_Expect((addr & 0xFF));
}
static void _WriteDummyByteExpect(void){
  SPI_writeByte_Expect(anyByte);
  SPI_writeByte_IgnoreArg_b();
}

static void _cmdWithOnlyOpExpect(uint8_t op){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(op);
  SPI_DeselectCS_Expect();
}

static void _cmdWithAddrExpect(uint8_t op){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(op);
  _WriteAddrExpect(addrToOperate);
  SPI_DeselectCS_Expect();
}

TEST_SETUP(AT26DF){
  MockSPI_Init();
  AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
  for(i = 0; i < len; i++)
    ++expectData[i];
  memset(actualData,0,len);
}

TEST_TEAR_DOWN(AT26DF){
  MockSPI_Verify();
  MockSPI_Destroy();

  addrToOperate >>= 1;
  if(addrToOperate == 0)
    addrToOperate = 0x343dfa;
}

TEST(AT26DF, ReadArray){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
  _WriteAddrExpect(addrToOperate);
  _WriteDummyByteExpect();
  for(i = 0; i < len; i++)
    SPI_readByte_ExpectAndReturn(expectData[i]);
  SPI_DeselectCS_Expect();

  AT26DF_readArray(addrToOperate,actualData,len);

  TEST_ASSERT_EQUAL_HEX8_ARRAY(expectData,actualData,len);
}

TEST(AT26DF, ReadArrayLowFreq){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READARRAY_LOWFREQ);
  _WriteAddrExpect(addrToOperate);
  for(i = 0; i < len; i++)
    SPI_readByte_ExpectAndReturn(expectData[i]);
  SPI_DeselectCS_Expect();

  AT26DF_readArrayLowFreq(addrToOperate,actualData,len);

  TEST_ASSERT_EQUAL_HEX8_ARRAY(expectData,actualData,len);
}

TEST(AT26DF, 0LenOrBufNULLDoNothing){
  uint8_t buf[1];
  AT26DF_readArray(anyAddr,buf,0);
  AT26DF_readArrayLowFreq(anyAddr,buf,0);
  AT26DF_readArray(anyAddr,NULL,1);
  AT26DF_readArrayLowFreq(anyAddr,NULL,1);
  AT26DF_programInPage(anyAddr,buf,0);
  AT26DF_programInPage(anyAddr,NULL,1);
}

TEST(AT26DF, programInPage){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_PROGRAM);
  _WriteAddrExpect(addrToOperate);
  for(i = 0; i < len; i++)
    SPI_writeByte_Expect(expectData[i]);
  SPI_DeselectCS_Expect();

  AT26DF_programInPage(addrToOperate,expectData,len);
}

TEST(AT26DF, blockErase4KB){
  _cmdWithAddrExpect(AT26DF_OPC_BLOCKERASE_4KB);

  AT26DF_blockErase4KB(addrToOperate);
}

TEST(AT26DF, blockErase32KB){
  _cmdWithAddrExpect(AT26DF_OPC_BLOCKERASE_32KB);

  AT26DF_blockErase32KB(addrToOperate);
}

TEST(AT26DF, blockErase64KB){
  _cmdWithAddrExpect(AT26DF_OPC_BLOCKERASE_64KB);

  AT26DF_blockErase64KB(addrToOperate);
}

TEST(AT26DF, chipErase){
  _cmdWithOnlyOpExpect(AT26DF_OPC_CHIPERASE);
   
  AT26DF_chipErase();
}

TEST(AT26DF, writeEnable){
  _cmdWithOnlyOpExpect(AT26DF_OPC_WRITEENABLE);

  AT26DF_writeEnable();
}

TEST(AT26DF, writeDisable){
  _cmdWithOnlyOpExpect(AT26DF_OPC_WRITEDISABLE);

  AT26DF_writeDisable();
}

TEST(AT26DF, protectSector){
  _cmdWithAddrExpect(AT26DF_OPC_PROTECTSECTOR);

  AT26DF_protectSector(addrToOperate);
}

TEST(AT26DF, unprotectSector){
  _cmdWithAddrExpect(AT26DF_OPC_UNPROTECTSECTOR);

  AT26DF_unprotectSector(addrToOperate);
}

TEST(AT26DF, readSPR){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READSPR);
  _WriteAddrExpect(addrToOperate);
  SPI_readByte_ExpectAndReturn(0xFF);
  SPI_DeselectCS_Expect();

  TEST_ASSERT_EQUAL_HEX8(0xFF,AT26DF_readSPR(addrToOperate));
}

TEST(AT26DF, readSR){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READSR);
  SPI_readByte_ExpectAndReturn(0x46);
  SPI_DeselectCS_Expect();

  TEST_ASSERT_EQUAL_HEX8(0x46,AT26DF_readSR());
}

static BOOL _SREqualTo3(uint8_t sr){
  return sr == 3;
}

TEST(AT26DF, pendSRCondition){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READSR);
  SPI_readByte_ExpectAndReturn(0x01);
  SPI_readByte_ExpectAndReturn(0x02);
  SPI_readByte_ExpectAndReturn(0x03);
  SPI_DeselectCS_Expect();

  TEST_ASSERT_EQUAL_HEX8(0x03,AT26DF_pendSRCondition(_SREqualTo3));
}

TEST(AT26DF, writeSR){
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_WRITESR);
  SPI_writeByte_Expect(0x82);
  SPI_DeselectCS_Expect();

  AT26DF_writeSR(0x82);
}

TEST(AT26DF, readDevInfo){
  AT26DF_DevInfo info;
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READDEVINFO);
  SPI_readByte_ExpectAndReturn(0x1F);
  SPI_readByte_ExpectAndReturn(0x47);
  SPI_readByte_ExpectAndReturn(0x00);
  SPI_DeselectCS_Expect();

  AT26DF_readDevInfo(&info);
  TEST_ASSERT_EQUAL_HEX8(0x1F,info.ManuID);
  TEST_ASSERT_EQUAL_HEX8(0x47,info.DevID[0]);
  TEST_ASSERT_EQUAL_HEX8(0x00,info.DevID[1]);
}

TEST(AT26DF, DeepPowerDown){
  _cmdWithOnlyOpExpect(AT26DF_OPC_DPD);

  AT26DF_deepPowerDown();
}

TEST(AT26DF, DeepPowerDownResume){
  _cmdWithOnlyOpExpect(AT26DF_OPC_DPD_RESUME);

  AT26DF_deepPowerDownResume();
}

AT26DFTestRunner.c

#include "unity_fixture.h"

TEST_GROUP_RUNNER(AT26DF)
{
  RUN_TEST_CASE(AT26DF, ReadArray);
  RUN_TEST_CASE(AT26DF, ReadArrayLowFreq);
  RUN_TEST_CASE(AT26DF, 0LenOrBufNULLDoNothing);
  RUN_TEST_CASE(AT26DF, programInPage);
  RUN_TEST_CASE(AT26DF, blockErase4KB);
  RUN_TEST_CASE(AT26DF, blockErase32KB);
  RUN_TEST_CASE(AT26DF, blockErase64KB);
  RUN_TEST_CASE(AT26DF, chipErase);
  RUN_TEST_CASE(AT26DF, writeEnable);
  RUN_TEST_CASE(AT26DF, writeDisable);
  RUN_TEST_CASE(AT26DF, protectSector);
  RUN_TEST_CASE(AT26DF, unprotectSector);
  RUN_TEST_CASE(AT26DF, readSPR);
  RUN_TEST_CASE(AT26DF, readSR);
  RUN_TEST_CASE(AT26DF, pendSRCondition);
  RUN_TEST_CASE(AT26DF, writeSR);
  RUN_TEST_CASE(AT26DF, readDevInfo);
  RUN_TEST_CASE(AT26DF, DeepPowerDown);
  RUN_TEST_CASE(AT26DF, DeepPowerDownResume);
}

测试结果:

结语

这一章中,我们终于把整个驱动模块整出来了。一个嵌入式驱动模块的开发过程居然可以完全没用到实际硬件,有没很神奇?只要我们对芯片手册的理解没有出现偏差,且芯片就是照着手册来着,那这个驱动模块就基本是正确的。那在下一章中,我们可以基于这个开发好的模块来写一些硬件测试,一方面验证硬件正确,一方面也验证了驱动模块的正确。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值