一. 前言
本文继续超级精简系列。
在嵌入式开发中IIC,UART,SPI等是非常常见的接口,其接口时序也很简单,虽然几乎所有的MCU都会有多个这些接口,但是有时候也会资源不够,硬件接口不够,这时我们就可以使用普通IO来模拟。另外一个好处就是使用IO模拟的可移植性,兼容性更强更简单灵活。
构建自己的一个IO模拟常见接口的代码库也显得很有意义,这一篇就来实现IO模拟IIC主机,后面还会继续分享,IO模拟SPI,UART等协议,实现自己的一个IO模拟常见接口的”轮子”。
我们提一下设计哲学是尽量简单,具备良好的可移植性,借鉴面向对象的设计思想。
二. IO模拟IIC接口设计
IIC协议本身就不再赘述了,比较简单,可以参考NXP的规格书《https://www.nxp.com.cn/docs/en/user-guide/UM10204.pdf》。
IIC接口只需要2根线,一个SCL时钟总是作为输出,一个SDA需要可读写。
所以IO模拟IIC要求准备两个IO,一个能输出即可,一个需要能配置为输入或者输出。
借鉴面向对象设计思想,模块化分层思想,我们先抽象出接口,面向接口编程而不是面向实现编程。先定好接口然后再尽心实现。
IIC接口很简单,很自然的就可以想到需要实现以下接口。
typedef void (*io_iic_scl_write_pf)(uint8_t val); /**< SCL写接口 */
typedef void (*io_iic_sda_write_pf)(uint8_t val); /**< SDA写接口 */
typedef void (*io_iic_sda_2read_pf)(void); /**< SDA转为读接口 */
typedef uint8_t (*io_iic_sda_read_pf)(void); /**< SDA读接口 */
typedef void (*io_iic_delay_us_pf)(uint32_t delay); /**< 延时接口 */
typedef void (*io_iic_init_pf)(void); /**< 初始化接口 */
typedef void (*io_iic_deinit_pf)(void); /**< 解除初始化接口 */
其中初始化接口和解除初始化接口非必须,但是我们加上是考虑设备的分时使用,类似对象的构造与析构函数。使用设备时先调用初始化,用完再解除初始化,完成资源释放。这样资源可以分时复用。
其中延时接口用于控制速度。
SCL只需要一个接口即写,用于产生SCL的时钟。
SDA接口这里需要实现3个接口,一个是输出,一个是输入,这里转为输入单独拎出来是因为,IIC读时需要先释放总线(转为读高阻态),以便对方去驱动。所以转为读到读之间有延时,不是同一时刻的行为,所以不能直接合并到读时才转为读,需要单独拎出来接口。
以上接口即模拟了IIC的行为即成员函数,然后加上其属性即成员变量延迟时间delayus,就形成了如下的对象。Delayus用于控制SCL的周期即速度。
/**
* \struct io_iic_dev_st
* 接口结构体
*/
typedef struct
{
io_iic_scl_write_pf scl_write; /**< scl写接口 */
io_iic_sda_write_pf sda_write; /**< sda写接口 */
io_iic_sda_2read_pf sda_2read; /**< sda转为读接口 */
io_iic_sda_read_pf sda_read; /**< sda读接口 */
io_iic_delay_us_pf delay_pf; /**< 延时接口 */
io_iic_init_pf init; /**< 初始化接口 */
io_iic_deinit_pf deinit; /**< 解除初始化接口 */
uint32_t delayus; /**< 延迟时间 */
} io_iic_dev_st;
以上即对下的依赖,即需要实现以上IO操作,基于此实现IIC接口操作。
而对于IIC接口我们也可以很自然的抽象到以下接口。
读/写字节
启动/停止信号
初始化/解除初始化。
/**
* \fn io_iic_start
* 发送启动信号
* \param[in] dev \ref io_iic_dev_st
*/
void io_iic_start(io_iic_dev_st* dev);
/**
* \fn io_iic_stop
* 发送停止信号
* \param[in] dev \ref io_iic_dev_st
*/
void io_iic_stop(io_iic_dev_st* dev);
/**
* \fn io_iic_write
* 写一个字节
* \param[in] dev \ref io_iic_dev_st
* \param[in] val 待写入的值
* \retval 0 写成功(收到了ACK)
* \retval -2 写失败(未收到ACK)
* \retval -1 参数错误
*/
int io_iic_write(io_iic_dev_st* dev, uint8_t val);
/**
* \fn io_iic_read
* 读一个字节
* \param[in] dev \ref io_iic_dev_st
* \param[out] val 存储读到的值
* \param[in] ack 1发送NACK 0发送ACK
* \retval 0 读成功
* \retval -1 参数错误
*/
int io_iic_read(io_iic_dev_st* dev, uint8_t* val, uint8_t ack);
/**
* \fn io_iic_init
* 初始化
* \param[in] dev \ref io_iic_dev_st
*/
void io_iic_init(io_iic_dev_st* dev);
/**
* \fn io_iic_deinit
* 解除初始化
* \param[in] dev \ref io_iic_dev_st
*/
void io_iic_deinit(io_iic_dev_st* dev);
三. 接口实现
上面已经设计好了接口,那么现在就来实现这些接口。
Start
实现如下,注释已经很清楚了,不再赘述
Stop
实现如下,注释已经很清楚了,不再赘述
Write
实现如下,注释已经很清楚了,不再赘述
Read
实现如下,注释已经很清楚了,不再赘述
Init/Deinit
实现本身没有需要特殊处理的,直接调用IO的初始化与解除初始化即可。
下一篇继续基于IO模拟的IIC接口实现EEPROM驱动与应用。