前言
上一章中,我们把模块的通信监控框架搭起来了。万事俱备,直接开始开发吧。
第一个接口
函数签名
我们来看看AT26DF的第一条命令:
嗯,Read Array,那第一个接口的名字就叫AT26DF_readArray了。
- 这个方法在CS断言后,先要求一个操作符,0B比03的好处是没有频率限制,那感情好。先写0B那个。
- 然后需要一个24bit的地址,那接口的参数中加一个地址参数。
- 因为选择了0B,还需要多传一个随便什么字符
- 然后就可以从这个地址起想读几个字符读几个了,那说明还要一个指定读多少字节的参数,以及一个告知把返回结果写到哪的参数
- 最后通过CS取消断言来结束
那这个接口的签名就基本确定了:
……
typedef uint32_t AT26DF_ADDR;
……
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len);
虽然用了32位的int表示了实际只需要24位的地址,但没事,只取低24位就好,主要比byte[3]用起来方便的多。
重新定义下类型后这个参数做什么用的也一目了然。完美!
开写测试用例
那我们接下来为他写测试用例,先把监听搭好,随便给几个参数(这里已经不需要那个NULL测试了,删掉吧):
AT26DFTest.c
#include "unity_fixture.h"
#include "AT26DF.h"
#include "MockSPI.h"
TEST_GROUP(AT26DF);
TEST_SETUP(AT26DF){
}
TEST_TEAR_DOWN(AT26DF){
}
TEST(AT26DF, ReadArray){
MockSPI_Init();
AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
AT26DF_readArray(0x345254, actualData, 5);
MockSPI_Verify();
MockSPI_Destroy();
}
生成试试。
咦,报错了,哦,光写了声明,没写实现。我们打开AT26DF.c文件,直接写个空函数先,TDD的基本原则就是没有写测试前不要写代码。
AT26DF.c
……
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
}
再次生成。
编译成功了,但这数量咋不对呢。哦,新测试忘记安装进Runner了。
AT26DFTestRunner.c
#include "unity_fixture.h"
TEST_GROUP_RUNNER(AT26DF)
{
RUN_TEST_CASE(AT26DF, ReadArray)
}
再生成。
数量对了。
然后往测试用例里头填内容。
首先应该要先断言:
……
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
SPI_SelectCS_Expect();
AT26DF_readArray(0x345254, actualData, 5);
……
生成:
果然出问题了,赶快回去在实现中加上这一句。
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
}
生成,通过!
好嘞,然后,首先应该传一个操作符0x0B
……
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
SPI_SelectCS_Expect();
SPI_writeByte_Expect(0x0B);
AT26DF_readArray(0x345254, actualData, 5);
……
再测试,又失败了
嗯,要回去加上写操作符了,但是为了让你明白mock真能测出你没按希望的参数调用,我们故意失误,打错了输出的错误码:
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(0x03);
}
这时来看看测试输出:
输出中准确地说明了测试用例失败的原因。
好啦,去改成正确的吧。
这时,我又觉得0x0B很不直观,这是个magic number,可读性差。所以我又改了改,给了它个名字,让它别那么magic了。
AT26DF.h
#ifndef _AT26DF_H
#define _AT26DF_H
#include <stdint.h>
#define AT26DF_OPC_READARRAY 0x0B
typedef uint32_t AT26DF_ADDR;
……
AT26DF.c
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(AT26DF_OPC_READARRAY);
}
AT26DFTest.c
……
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
SPI_SelectCS_Expect();
SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
AT26DF_readArray(0x345254, actualData, 5);
……
这样,以后的操作码我也都会用宏定义的方式给它个AT26DF_OPC_XXXX形式的名字。在测试用例和接口实现中都用名字的坏处是如果定义错了,你难以发现。但是没事,在硬件测试中会发现的。为了可读性还是值得的。
接下来,地址,传过去的是0x345254,那也应该收到对应的:
……
SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
// address byte
SPI_writeByte_Expect(0x34);
SPI_writeByte_Expect(0x52);
SPI_writeByte_Expect(0x54);
AT26DF_readArray(0x345254, actualData, 5);
……
测试,失败,然后修改实现:
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(AT26DF_OPC_READARRAY);
_spiWrite((addr >> 16) & 0xFF);
_spiWrite((addr >> 8) & 0xFF);
_spiWrite(addr & 0xFF);
}
测试通过。
然后应该写一个无所谓什么字节,CMock中是这么写的
SPI_writeByte_Expect(0x54);
// one don't care byte
SPI_writeByte_Expect(0xab);
SPI_writeByte_IgnoreArg_b();
AT26DF_readArray(0x345254, actualData, 5);
SPI_writeByte_IgnoreArg_b使得上一个SPI_writeByte_Expect忽视参数b。
这里又有magic number的问题了,为了让后人看到这个0xab不感到困惑,咱们略作修改。
……
TEST_GROUP(AT26DF);
static const uint8_t anyByte = 0xEE;
TEST_SETUP(AT26DF){
}
……
SPI_writeByte_Expect(0x54);
// one don't care byte
SPI_writeByte_Expect(anyByte);
SPI_writeByte_IgnoreArg_b();
AT26DF_readArray(0x345254, actualData, 5);
然后又是,测试,失败,然后改造实现:
……
#define DONTCAREBYTE 0x37
……
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(AT26DF_OPC_READARRAY);
_spiWrite((addr >> 16) & 0xFF);
_spiWrite((addr >> 8) & 0xFF);
_spiWrite(addr & 0xFF);
_spiWrite(DONTCAREBYTE);
}
好了,这个没用的字符发出去后就该开始读取数据了。
读取多少个是由len参数决定的,而返回的应该就是SPI_Read给他的,换句话说就是咱们的接口应该调用len次SPI_read然后把结果写到buf指向的数组中。
所以测试用例变成了这样:
TEST(AT26DF, ReadArray){
int i;
const uint8_t expectData[] = {0x12, 0x34, 0x56, 0x78, 0x9a};
uint8_t actualData[sizeof(expectData)];
const uint8_t len = sizeof(expectData);
MockSPI_Init();
AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
SPI_SelectCS_Expect();
SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
// address byte
SPI_writeByte_Expect(0x34);
SPI_writeByte_Expect(0x52);
SPI_writeByte_Expect(0x54);
// one don't care byte
SPI_writeByte_Expect(anyByte);
SPI_writeByte_IgnoreArg_b();
for(i = 0; i < len; i++){
SPI_readByte_ExpectAndReturn(expectData[i]);
}
AT26DF_readArray(0x345254,actualData,len);
TEST_ASSERT_EQUAL_HEX8_ARRAY(expectData,actualData,len);
MockSPI_Verify();
MockSPI_Destroy();
}
我们要验证了接口调用了N次的read,同时真的把结果返回来了。
测试,失败,然后改造实现:
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(AT26DF_OPC_READARRAY);
_spiWrite((addr >> 16) & 0xFF);
_spiWrite((addr >> 8) & 0xFF);
_spiWrite(addr & 0xFF);
_spiWrite(DONTCAREBYTE);
_spiBurstRead(buf,len);
}
测试通过,OK。最后别漏了个取消断言:
……
for(i = 0; i < sizeof(expectData); i++){
SPI_readByte_ExpectAndReturn(expectData[i]);
}
SPI_DeselectCS_Expect();
AT26DF_readArray(0x345254,actualData,sizeof(expectData));
……
然后接口实现就成了这样:
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(AT26DF_OPC_READARRAY);
_spiWrite((addr >> 16) & 0xFF);
_spiWrite((addr >> 8) & 0xFF);
_spiWrite(addr & 0xFF);
_spiWrite(DONTCAREBYTE);
_spiBurstRead(buf,len);
_csDesel();
}
这样,我们的第一个测试用例其实就写完了。
我们回头再看看写好的测试用例,
MockSPI_Init();
AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
和
MockSPI_Verify();
MockSPI_Destroy();
其实是这个模块的所有测试通用的Setup和teardown部分,所以,我们再进行修改,把这两部分分别移到:TEST_SETUP和TEST_TEAR_DOWN里,这样,在所有这个测试组的测试用例开始前都会初始化MockSPI并注册,结束时都会验证MockSPI并回收资源。于是,到目前为止,我们的几个文件长这样:
本章快照
AT26DFTest.c
#include "unity_fixture.h"
#include "AT26DF.h"
#include "MockSPI.h"
TEST_GROUP(AT26DF);
static const uint8_t anyByte = 0xEE;
TEST_SETUP(AT26DF){
MockSPI_Init();
AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
}
TEST_TEAR_DOWN(AT26DF){
MockSPI_Verify();
MockSPI_Destroy();
}
TEST(AT26DF, ReadArray){
int i;
const uint8_t expectData[] = {0x12, 0x34, 0x56, 0x78, 0x9a};
uint8_t actualData[sizeof(expectData)];
const uint8_t len = sizeof(expectData);
SPI_SelectCS_Expect();
SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
// address byte
SPI_writeByte_Expect(0x34);
SPI_writeByte_Expect(0x52);
SPI_writeByte_Expect(0x54);
// one don't care byte
SPI_writeByte_Expect(anyByte);
SPI_writeByte_IgnoreArg_b();
for(i = 0; i < len; i++){
SPI_readByte_ExpectAndReturn(expectData[i]);
}
SPI_DeselectCS_Expect();
AT26DF_readArray(0x345254,actualData,len);
TEST_ASSERT_EQUAL_HEX8_ARRAY(expectData,actualData,len);
}
AT26DFTestRunner.c
#include "unity_fixture.h"
TEST_GROUP_RUNNER(AT26DF)
{
RUN_TEST_CASE(AT26DF, ReadArray);
}
AT26DF.h
#ifndef _AT26DF_H
#define _AT26DF_H
#include <stdint.h>
#define AT26DF_OPC_READARRAY 0x0B
typedef uint32_t AT26DF_ADDR;
// description: Register SPI call back functions
// parameter : spi_rb callback function to read byte using SPI
// spi_wb callback function to write byte using SPI
// return :
// note : you should register SPI functions through this interface before use the driver.
void AT26DF_regFuncSPI(uint8_t (*spi_rb)(void), void (*spi_wb)(uint8_t wb));
// description: Register SPI call back functions
// parameter : spi_rb callback function to burst read byte using SPI
// NULL if want to use the default function
// spi_wb callback function to burst write byte using SPI
// NULL if want to use the default function
// return :
// note : if you don't register any functions, the default functions will call the functions
// registered by the AT26DF_regFuncSPI.
void AT26DF_regFuncSPIBurst(void (*spi_rb)(uint8_t* pBuf, uint16_t len),
void (*spi_wb)(const uint8_t* pBuf, uint16_t len));
// description: Registers call back function for AT26DF select & deselect.
// parameter : cs_sel callback function for WIZCHIP select
// cs_desel callback fucntion for WIZCHIP deselect
// return :
// note : you should register CS functions through this interface before use the driver.
void AT26DF_regFuncCS(void(*cs_sel)(void), void(*cs_desel)(void));
// description: Sequentially read a continuous stream of data begun from the address specified.
// parameter : addr the starting address
// buf buffer to store the result.
// len the length of data you want to read.
// return :
// note : this function can be used at any SCK frequency up to the maximum specified by fSCK.
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len);
#endif
AT26DF.c
#include "common.h"
#include "AT26DF.h"
#define DONTCAREBYTE 0x37
static uint8_t _spi_rb_default(void);
static void _spi_wb_default(uint8_t wb);
static void _spi_burst_rb_default(uint8_t* pBuf, uint16_t len);
static void _spi_burst_wb_default(const uint8_t* pBuf, uint16_t len);
static void _nullFunc(void);
static uint8_t (*_spiRead)(void) = _spi_rb_default;
static void (*_spiWrite)(uint8_t wb) = _spi_wb_default;
static void (*_spiBurstRead)(uint8_t* pBuf, uint16_t len) = _spi_burst_rb_default;
static void (*_spiBurstWrite)(const uint8_t* pBuf, uint16_t len) = _spi_burst_wb_default;
static void(*_csSel)(void) = _nullFunc;
static void(*_csDesel)(void) = _nullFunc;
static uint8_t _spi_rb_default(void){return 0;}
static void _spi_wb_default(uint8_t wb){(void)wb;}
static void _spi_burst_rb_default(uint8_t* pBuf, uint16_t len){
while(len > 0){
--len;
*pBuf++ = _spiRead();
}
}
static void _spi_burst_wb_default(const uint8_t* pBuf, uint16_t len){
while(len > 0){
--len;
_spiWrite(*pBuf++);
}
}
static void _nullFunc(void){}
void AT26DF_regFuncSPI(uint8_t (*spi_rb)(void), void (*spi_wb)(uint8_t wb)){
_spiRead = spi_rb;
_spiWrite = spi_wb;
}
void AT26DF_regFuncSPIBurst(void (*spi_rb)(uint8_t* pBuf, uint16_t len), void (*spi_wb)(const uint8_t* pBuf, uint16_t len)){
if(spi_rb)
_spiBurstRead = spi_rb;
else
_spiBurstRead = _spi_burst_rb_default;
if(spi_wb)
_spiBurstWrite = spi_wb;
else
_spiBurstWrite = _spi_burst_wb_default;
}
void AT26DF_regFuncCS(void(*cs_sel)(void), void(*cs_desel)(void)){
_csSel = cs_sel;
_csDesel = cs_desel;
}
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
_csSel();
_spiWrite(AT26DF_OPC_READARRAY);
_spiWrite((addr >> 16) & 0xFF);
_spiWrite((addr >> 8) & 0xFF);
_spiWrite(addr & 0xFF);
_spiWrite(DONTCAREBYTE);
_spiBurstRead(buf,len);
_csDesel();
}
结语
这一章中,我较为详尽的展示了用TDD的方式开发单个接口的各个步骤。不断的重复TDD微循环:
- 添加一个小测试
- 运行所有测试并故意看到新的那个失败,甚至编译不了。
- 做很小的修改使得测试通过。
- 运行所有测试并看到新的一个通过。
- 重构,移除冗余并提升代码可读性。
测试用例也遵从 四步测试模式:
- Setup:建立测试的预环境(precondition)
- Exercise:做一些事情
- Verify:检查输出是否如期望的
- Cleanup:在测试后把系统还原为最初状态
感兴趣的朋友可以读《测试驱动的嵌入式c语言开发》更深入的学习,这是我的笔记。
后面的章节中,我不再事无巨细的按照一个个测试的小步骤来讲,而可能会更偏向于重构的思想来讲。但是读者要清楚,实际上还是遵从着这章中的模式一点点递增地开发出程序的。