一. 前言
本文继续超级精简的IO模拟常见总线接口系列。
前面实现了IO模拟IIC主机并进行了EEPROM的读写测试,基于命令行实现了EEPROM的编辑工具。见:《https://mp.weixin.qq.com/s/ESzWWqxHpQevsWfjV0s2VQ》《https://mp.weixin.qq.com/s/vlhy7z8XrgxXJoAtYuXw0g》
这一篇继续以上的套路,实现IO模拟SPI主机,进行SPIFLASH的读写测试,基于命令行实现SPIFLASH的编辑工具。
我们还是坚持简单的设计哲学,尽量具备良好的可移植性,借鉴面向对象的设计思想。
二. IO模拟SPI接口设计
SPI协议本身就不再赘述了,比较简单,可以以下链接的说明,重点理解4种模式即CPHA和CPOL这两个参数的含义,可以参考链接:
https://onlinedocs.microchip.com/pr/GUID-835917AF-E521-4046-AD59-DCB458EB8466-en-US-1/index.html?GUID-E4682943-46B9-4A20-A62C-33E8FD3343A3。
SPI接口只需要4根线,SCK,CS,MOSI,MISO,对于主机除了MISO是输入其他的都是输出。
所以IO模拟SPI要求准备4个IO,一个输入三个输出。
借鉴面向对象设计思想,模块化分层思想,我们先抽象出接口,面向接口编程而不是面向实现编程。先定好接口然后再进行实现。
SPI接口很简单,很自然的就可以想到需要实现以下接口。
即CS SCK MOSI的输出操作接口,MISO的读接口。另外演示接口用于控制速度,初始化和解除初始化接口用于对应资源的初始化与释放。
typedef void (*io_spi_cs_write_pf)(uint8_t val); /**< CS写接口 */
typedef void (*io_spi_sck_write_pf)(uint8_t val); /**< SCK写接口 */
typedef void (*io_spi_mosi_write_pf)(uint8_t val); /**< MOSI写接口 */
typedef uint8_t (*io_spi_miso_read_pf)(void); /**< MISO读接口 */
typedef void (*io_spi_delay_ns_pf)(uint32_t delay); /**< 延时接口 */
typedef void (*io_spi_init_pf)(void); /**< 初始化接口 */
typedef void (*io_spi_deinit_pf)(void); /**< 解除初始化接口 */
那么应用面向对象思想,将上述功能接口即行为,封装为设备类。
同时增加了几个成员变量
delayns用以控制速度即SCK的电平保持时间
mode指定0~3的模式
msb指定是否高位在前
/**
* \struct io_spi_dev_st
* 接口结构体
*/
typedef struct
{
io_spi_cs_write_pf cs_write; /**< cs写接口 */
io_spi_sck_write_pf sck_write; /**< sck写接口 */
io_spi_mosi_write_pf mosi_write; /**< mosi写接口 */
io_spi_miso_read_pf miso_read; /**< miso读接口 */
io_spi_delay_ns_pf delay_pf; /**< 延时接口 */
io_spi_init_pf init; /**< 初始化接口 */
io_spi_deinit_pf deinit; /**< 解除初始化接口 */
uint32_t delayns; /**< 延迟时间 */
uint8_t mode; /**< 模式0~3 bit0 CPHA bit1 CPOL */
uint8_t msb; /**< 1高位在前 否则低位在前 */
} io_spi_dev_st;
那么自然而然就可以抽象出以下对外接口
Enable和disable即控制CS拉低和拉高。
Init和deinit即对应资源的初始化和释放。
核心就是trans用于实现发送的同时和接收数据,可以指定tx和rx来指定是否要发送或者接收。
/**
* \fn io_spi_enable
* 发送CS使能信号,拉低CS
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_enable(io_spi_dev_st* dev);
/**
* \fn io_spi_disable
* 拉高CS,取消片选
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_disable(io_spi_dev_st* dev);
/**
* \fn io_spi_trans
* 传输,发送的同时读
* \param[in] dev \ref io_spi_dev_st
* \param[in] tx 待发送的数据 如果tx为空则默认发送FF
* \param[out] rx 存储接收的数据 如果rx为空则不读
* \param[in] size 传输的字节数
* \retval 0 读成功
* \retval -1 参数错误
*/
int io_spi_trans(io_spi_dev_st* dev, uint8_t* tx, uint8_t* rx, uint32_t size);
/**
* \fn io_spi_init
* 初始化
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_init(io_spi_dev_st* dev);
/**
* \fn io_spi_deinit
* 解除初始化
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_deinit(io_spi_dev_st* dev);
三. 接口实现
上面已经设计好了接口,那么现在就来实现这些接口。
Init/Deinit
实现本身没有需要特殊处理的,直接调用IO的初始化与解除初始化即可。
void io_spi_init(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->init != 0))
{
dev->init();
}
}
void io_spi_deinit(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->deinit != 0))
{
dev->deinit();
}
}
Enable/disable
即拉低和拉高CS,这里拉低CS前将SCK设置为空闲电平
void io_spi_enable(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->cs_write != 0) && (dev->sck_write != 0))
{
/* 准备空闲时的SCK状态,在CS拉低之前准备好 */
dev->sck_write((dev->mode & 0x02) >> 1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayns);
}
/* 拉低CS */
dev->cs_write(0);
/* (5) SCK电平保持 */
//if(dev->delay_pf != 0)
//{
// dev->delay_pf(dev->delayns);
//}
}
}
void io_spi_disable(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->cs_write != 0))
{
dev->cs_write(1);
}
}
io_spi_trans
核心实现就是trans的实现,考虑msb的配置,不同模式的配置。
可以一次传输多个字节,就是单个字节的循环即可。
所以关键是实现一个字节的发送与接收。
先整理出时序图
那么发送n个字节实际就是循环n次
int io_spi_trans(io_spi_dev_st* dev, uint8_t* tx, uint8_t* rx, uint32_t size)
{
uint32_t i = 0; /* 字节数循环 */
uint8_t j = 0; /* 位数循环 */
uint8_t msb = 0; /* MSB标志 */
uint8_t cpha = 0; /* 相位标志bit0 */
uint8_t cpol = 0; /* 极性标志bit1 */
uint8_t rx_val = 0; /* 发送字节缓存 */
uint8_t tx_val = 0; /* 接收字节缓存 */
if(dev == 0)
{
return -1;
}
if((dev->miso_read == 0) || (dev->mosi_write == 0) || (dev->sck_write == 0))
{
/* dev->delay_pf 可以不实现 */
return -1;
}
cpha = dev->mode & 0x01;
msb = dev->msb;
/* (1) 准备空闲时的SCK状态 */
cpol = (dev->mode & 0x02) >> 1;
/* 这一句其实可以不用,和io_spi_enable效果一样,这里仅需要初始化cpol局部变量即可
* 加上这一句可以在此确保SCK引脚状态初始化,可靠性角度来说加上提高冗余.
*/
dev->sck_write(cpol);
for(i=0; i<size; i++)
{
......
这里是收发一个字节的实现
}
return 0;
}
而收发一个字节又可以分解为收发一个bit的实现
/* 取待发送的值, 用户没有提供则发送0xFF */
if(tx != 0)
{
tx_val = *tx++;
}
else
{
tx_val = 0xFF;
}
/* 接收到的值初始化 */
rx_val = 0;
for(j=0 ;j<8; j++)
{
......
这里是收发一个bit的实现
}
/* 存储读到的值 */
if(rx != 0)
{
*rx++ = rx_val;
}
于是最后实现一个bit的收发实现,
通过cpol初始值,然后进行异或1进行反转产生SCK时钟。
先准备MOSI的数据发送,根据msb考虑移位取数的方向,并且如果tx没有则发送固定的0xFF。
然后接收要考虑CPHA是第一个边沿还是第二个边沿采样,也要根据msb决定移动方向,最后根据rx是否存储接收的数据。
/* (2)对于发送,不管对方哪个边沿采样,都是都在第一个边沿之前准备好MOSI就行
* 如果对于对方第一个边沿采样,这里修改MOSI之后最好有个数据建立时间
*/
if(msb)
{
dev->mosi_write(tx_val & 0x80); /* 高位在前,先发送高位,未发送数据再往高位移动 */
tx_val <<= 0x1; /* 注意写的时候是先写后移位 */
}
else
{
dev->mosi_write(tx_val & 0x01); /* 低位在前,先发送低位,未发送数据再往高位移动 */
tx_val >>= 0x1;
}
/* (3)反转产生第1个CLK边沿 */
cpol ^= 0x01;
dev->sck_write(cpol);
if(rx != 0)
{
if(cpha == 0)
{
/* (4)第一个边沿采样 */
if(msb)
{
rx_val <<= 0x1; /* 注意读的时候是先移位后读 */
rx_val |= dev->miso_read(); /* 高位在前,先读到低位,已接收数据再往高位移动 */
}
else
{
rx_val >>= 0x1;
rx_val |= dev->miso_read() <<7; /* 低位在前,先读到高位,已接收数据再往低位移动 */
}
}
}
/* (5) SCK电平保持 */
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayns);
}
/* (6)反转产生第2个CLK边沿 */
cpol ^= 0x01;
dev->sck_write(cpol);
if(rx != 0)
{
if(cpha == 1)
{
/* (7) 第2个边沿采样 */
if(msb)
{
rx_val <<= 0x1;
rx_val |= dev->miso_read(); /* 高位在前,先读到低位再往高位移动 */
}
else
{
rx_val >>= 0x1;
rx_val |= dev->miso_read()<<7; /* 低位在前,先读到高位再往低位移动 */
}
}
}
/* (5) SCK电平保持 */
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayns);
}
总的代码如下
io_spi.c
#include "io_spi.h"
void io_spi_enable(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->cs_write != 0) && (dev->sck_write != 0))
{
/* 准备空闲时的SCK状态,在CS拉低之前准备好 */
dev->sck_write((dev->mode & 0x02) >> 1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayns);
}
/* 拉低CS */
dev->cs_write(0);
/* (5) SCK电平保持 */
//if(dev->delay_pf != 0)
//{
// dev->delay_pf(dev->delayns);
//}
}
}
void io_spi_disable(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->cs_write != 0))
{
dev->cs_write(1);
}
}
/**
* _____ _____
* CS |_____________________________________________________________|
* _____________ _________
* SCK(CPOL=0) xx__________| |___ xxx __________| |__________
* __________ ____xxx __________ __________
* SCK(CPOL=1) xx |_____________| |_________|
* (0)
* (1)
* (2)
* (3)(4)
* (5)
* (6)(7)
* MISO ^ ^
* MOSI ^
* (1) (2) (4) (6)
* (3) (5)
* 其中()表示行为,^表示MOSI/MISO的输出或者采样位置.
* (0) io_spi_enable 准备SCK空闲状态,拉低CS.
* (1) 准备SCK初始状态,和(0)时SCK初始状态一样,代码中执行这个操作的目的仅仅是初始化局部变量cpol而已.
* (2) 输出MOSI数据.
* (3) 反转SCK产生第1个边沿.
* (4) 如果CPHA=0 则第1个边沿采样,MISO在此采样.
* (5) SCK高/低电平保持时间.
* (6) 反转SCK产生第2个边沿.
* (7) 如果CPHA=1 则第2个边沿采样,MISO在此采样.
*/
int io_spi_trans(io_spi_dev_st* dev, uint8_t* tx, uint8_t* rx, uint32_t size)
{
uint32_t i = 0; /* 字节数循环 */
uint8_t j = 0; /* 位数循环 */
uint8_t msb = 0; /* MSB标志 */
uint8_t cpha = 0; /* 相位标志bit0 */
uint8_t cpol = 0; /* 极性标志bit1 */
uint8_t rx_val = 0; /* 发送字节缓存 */
uint8_t tx_val = 0; /* 接收字节缓存 */
if(dev == 0)
{
return -1;
}
if((dev->miso_read == 0) || (dev->mosi_write == 0) || (dev->sck_write == 0))
{
/* dev->delay_pf 可以不实现 */
return -1;
}
cpha = dev->mode & 0x01;
msb = dev->msb;
/* (1) 准备空闲时的SCK状态 */
cpol = (dev->mode & 0x02) >> 1;
/* 这一句其实可以不用,和io_spi_enable效果一样,这里仅需要初始化cpol局部变量即可
* 加上这一句可以在此确保SCK引脚状态初始化,可靠性角度来说加上提高冗余.
*/
dev->sck_write(cpol);
for(i=0; i<size; i++)
{
/* 取待发送的值, 用户没有提供则发送0xFF */
if(tx != 0)
{
tx_val = *tx++;
}
else
{
tx_val = 0xFF;
}
/* 接收到的值初始化 */
rx_val = 0;
for(j=0 ;j<8; j++)
{
/* (2)对于发送,不管对方哪个边沿采样,都是都在第一个边沿之前准备好MOSI就行
* 如果对于对方第一个边沿采样,这里修改MOSI之后最好有个数据建立时间
*/
if(msb)
{
dev->mosi_write(tx_val & 0x80); /* 高位在前,先发送高位,未发送数据再往高位移动 */
tx_val <<= 0x1; /* 注意写的时候是先写后移位 */
}
else
{
dev->mosi_write(tx_val & 0x01); /* 低位在前,先发送低位,未发送数据再往高位移动 */
tx_val >>= 0x1;
}
/* (3)反转产生第1个CLK边沿 */
cpol ^= 0x01;
dev->sck_write(cpol);
if(rx != 0)
{
if(cpha == 0)
{
/* (4)第一个边沿采样 */
if(msb)
{
rx_val <<= 0x1; /* 注意读的时候是先移位后读 */
rx_val |= dev->miso_read(); /* 高位在前,先读到低位,已接收数据再往高位移动 */
}
else
{
rx_val >>= 0x1;
rx_val |= dev->miso_read() <<7; /* 低位在前,先读到高位,已接收数据再往低位移动 */
}
}
}
/* (5) SCK电平保持 */
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayns);
}
/* (6)反转产生第2个CLK边沿 */
cpol ^= 0x01;
dev->sck_write(cpol);
if(rx != 0)
{
if(cpha == 1)
{
/* (7) 第2个边沿采样 */
if(msb)
{
rx_val <<= 0x1;
rx_val |= dev->miso_read(); /* 高位在前,先读到低位再往高位移动 */
}
else
{
rx_val >>= 0x1;
rx_val |= dev->miso_read()<<7; /* 低位在前,先读到高位再往低位移动 */
}
}
}
/* (5) SCK电平保持 */
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayns);
}
}
/* 存储读到的值 */
if(rx != 0)
{
*rx++ = rx_val;
}
}
return 0;
}
void io_spi_init(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->init != 0))
{
dev->init();
}
}
void io_spi_deinit(io_spi_dev_st* dev)
{
if((dev != 0) && (dev->deinit != 0))
{
dev->deinit();
}
}
io_spi.h
extern "C"{
typedef void (*io_spi_cs_write_pf)(uint8_t val); /**< CS写接口 */
typedef void (*io_spi_sck_write_pf)(uint8_t val); /**< SCK写接口 */
typedef void (*io_spi_mosi_write_pf)(uint8_t val); /**< MOSI写接口 */
typedef uint8_t (*io_spi_miso_read_pf)(void); /**< MISO读接口 */
typedef void (*io_spi_delay_ns_pf)(uint32_t delay); /**< 延时接口 */
typedef void (*io_spi_init_pf)(void); /**< 初始化接口 */
typedef void (*io_spi_deinit_pf)(void); /**< 解除初始化接口 */
/**
* \struct io_spi_dev_st
* 接口结构体
*/
typedef struct
{
io_spi_cs_write_pf cs_write; /**< cs写接口 */
io_spi_sck_write_pf sck_write; /**< sck写接口 */
io_spi_mosi_write_pf mosi_write; /**< mosi写接口 */
io_spi_miso_read_pf miso_read; /**< miso读接口 */
io_spi_delay_ns_pf delay_pf; /**< 延时接口 */
io_spi_init_pf init; /**< 初始化接口 */
io_spi_deinit_pf deinit; /**< 解除初始化接口 */
uint32_t delayns; /**< 延迟时间 */
uint8_t mode; /**< 模式0~3 bit0 CPHA bit1 CPOL */
uint8_t msb; /**< 1高位在前 否则低位在前 */
} io_spi_dev_st;
/**
* \fn io_spi_enable
* 发送CS使能信号,拉低CS
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_enable(io_spi_dev_st* dev);
/**
* \fn io_spi_disable
* 拉高CS,取消片选
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_disable(io_spi_dev_st* dev);
/**
* \fn io_spi_trans
* 传输,发送的同时读
* \param[in] dev \ref io_spi_dev_st
* \param[in] tx 待发送的数据 如果tx为空则默认发送FF
* \param[out] rx 存储接收的数据 如果rx为空则不读
* \param[in] size 传输的字节数
* \retval 0 读成功
* \retval -1 参数错误
*/
int io_spi_trans(io_spi_dev_st* dev, uint8_t* tx, uint8_t* rx, uint32_t size);
/**
* \fn io_spi_init
* 初始化
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_init(io_spi_dev_st* dev);
/**
* \fn io_spi_deinit
* 解除初始化
* \param[in] dev \ref io_spi_dev_st
*/
void io_spi_deinit(io_spi_dev_st* dev);
}