嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 超级精简系列之九:超级精简的IO模拟SPI的C实现

超级精简系列之九:超级精简的IO模拟SPI的C实现

一. 前言

本文继续超级精简的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种模式即CPHACPOL这两个参数的含义,可以参考链接:

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要求准备4IO,一个输入三个输出。

借鉴面向对象设计思想,模块化分层思想,我们先抽象出接口,面向接口编程而不是面向实现编程。先定好接口然后再进行实现。

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;

那么自然而然就可以抽象出以下对外接口

Enabledisable即控制CS拉低和拉高。

Initdeinit即对应资源的初始化和释放。

核心就是trans用于实现发送的同时和接收数据,可以指定txrx来指定是否要发送或者接收。

/**
* \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

#ifndef IO_SPI_H#define IO_SPI_H
#ifdef __cplusplus extern "C"{#endif
#include <stdint.h>
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);
#ifdef __cplusplus }#endif
#endif