背景
前面分析了硬件SPI框架,明白了RTT硬件SPI的框架。而我们实际应用中,不少设备并不支持SPI,或者在做项目时,所选型的器件硬件SPI接口不够,而有需要SPI功能,这个时候可以考虑做软件SPI。而rtthread框架下,在组件层也实现了master模式的SPI适配(在这里,还是想吐槽一下现在的软件SPI实现,完全可以组件层实现的玩意,非得把gpio控制部分封装到驱动层去,纯粹的多此一举)。
RTT软件SPI框架解析
相比较于软件I2C,个人认为软件SPI实现比软件I2C简单,本质上软件SPI还是clk和data的组合,存在三线制和四线制的区别。另外,SPI存在四种工作模式的设置。相比较于I2C通信各种加地址,加ack检测,SPI仅 仅通过CS脚的高低电平就实现了这部分处理,降低了总线资源消耗,更重要的是,SPI总线的通信逻辑变得异常简单。
公共入口
static const struct rt_spi_ops spi_bit_bus_ops = { .configure = spi_bit_configure, .xfer = spi_bit_xfer, }; rt_err_t rt_spi_bit_add_bus(struct rt_spi_bit_obj *obj, const char *bus_name, struct rt_spi_bit_ops *ops) { obj->ops = ops; obj->config.data_width = 8; obj->config.max_hz = 1 * 1000 * 1000; obj->config.mode = RT_SPI_MASTER | RT_SPI_MSB | RT_SPI_MODE_0; /* idle status */ if (obj->config.mode & RT_SPI_CPOL) SCLK_H(ops); else SCLK_L(ops); // 注册SPI总线设备并返回执行结果,此接口与硬件SPI注册接口一致 return rt_spi_bus_register(&obj->bus, bus_name, &spi_bit_bus_ops); }
注册接口
从代码上看,软件SPI实际上还是和硬件SPI共用一套框架,对上层来说还是一样的接口,但由于其是纯逻辑的东西,因此不适合放置于驱动层实现。
另外,我们会发现一个问题,软件SPI的入口,居然是带一堆参数的,而组件层并未写这块的入口调用,因此为了适配软件SPI,bsp层不得不对应的实现一套ops实现,这也是我吐槽的点,为啥一个纯逻辑的东西还留一些接口放硬件驱动层。
SPI配置接口
rt_err_t spi_bit_configure(struct rt_spi_device *device, struct rt_spi_configuration *configuration) { struct rt_spi_bit_obj *obj = rt_container_of(device->bus, struct rt_spi_bit_obj, bus); struct rt_spi_bit_ops *ops = obj->ops; RT_ASSERT(device != RT_NULL); RT_ASSERT(configuration != RT_NULL); // 如果定义了相关io口的初始化,则初始化spi相关io口 if(ops->pin_init != RT_NULL) { ops->pin_init(); } // 未实现slave 模式的软件spi if (configuration->mode & RT_SPI_SLAVE) { return -RT_EIO; } // 工作模式设置 if (configuration->mode & RT_SPI_CPOL) { SCLK_H(ops); } else { SCLK_L(ops); } // 不同速率下的延时设置 if (configuration->max_hz < 200000) { ops->delay_us = 1; } else { ops->delay_us = 0; } // 把配置信息存储至obj->config,以便后续不同线程调用时使用 rt_memcpy(&obj->config, configuration, sizeof(struct rt_spi_configuration)); return RT_EOK; }
从配置函数上看,我们已经知道软件spi不支持从模式,而实际上,根据RTT文档描述,整个RTT的SPI框架都未考虑spi从模式这种应用场景,在使用时需要注意这块。
SPI传输接口
rt_ssize_t spi_bit_xfer(struct rt_spi_device *device, struct rt_spi_message *message) { struct rt_spi_bit_obj *obj = rt_container_of(device->bus, struct rt_spi_bit_obj, bus); struct rt_spi_bit_ops *ops = obj->ops; struct rt_spi_configuration *config = &obj->config; rt_base_t cs_pin = device->cs_pin; RT_ASSERT(device != NULL); RT_ASSERT(message != NULL); #ifdef RT_SPI_BITOPS_DEBUG if (!ops->tog_sclk || !ops->set_sclk || !ops->get_sclk) { LOG_E("SPI bus error, SCLK line not defined"); } if (!ops->set_mosi || !ops->get_mosi) { LOG_E("SPI bus error, MOSI line not defined"); } if (!ops->set_miso || !ops->get_miso) { LOG_E("SPI bus error, MISO line not defined"); } #endif /* 如果定义了cs脚,则拉低cs信号,代表可以传输 */ if (message->cs_take && (cs_pin != PIN_NONE)) { LOG_I("spi take cs\n"); rt_pin_write(cs_pin, PIN_LOW); spi_delay(ops); /* spi时钟相位初始化 */ if (config->mode & RT_SPI_CPHA) { spi_delay(ops); TOG_SCLK(ops); } } // 数据传输实现 if (config->mode & RT_SPI_3WIRE) { if (config->data_width <= 8) { spi_xfer_3line_data8(ops, config, message->send_buf, message->recv_buf, message->length); } else if (config->data_width <= 16) { spi_xfer_3line_data16(ops, config, message->send_buf, message->recv_buf, message->length); } } else { if (config->data_width <= 8) { spi_xfer_4line_data8(ops, config, message->send_buf, message->recv_buf, message->length); } else if (config->data_width <= 16) { spi_xfer_4line_data16(ops, config, message->send_buf, message->recv_buf, message->length); } } /* 释放 CS信号,结束spi写 */ if (message->cs_release && (cs_pin != PIN_NONE)) { spi_delay(ops); rt_pin_write(cs_pin, PIN_HIGH); LOG_I("spi release cs\n"); } return message->length; }
三线制SPI
在初始化spi设备时,有一个参数obj->config.mode,此参数初始化时,如果给了 RT_SPI_3WIRE这个配置,则代表此SPI总线工作在3线制模式。而对应的实现入口如下:
// spi_bit_xfer --> spi_xfer_3line_data8 // config->data_width <= 8 // --> spi_xfer_3line_data16 // 8 < config->data_width <= 16
spi_xfer_3line_data8
rt_inline rt_ssize_t spi_xfer_3line_data8(struct rt_spi_bit_ops *ops, struct rt_spi_configuration *config, const void *send_buf, void *recv_buf, rt_size_t length) { int i = 0; RT_ASSERT(ops != RT_NULL); RT_ASSERT(length != 0); { const rt_uint8_t *send_ptr = send_buf; rt_uint8_t *recv_ptr = recv_buf; rt_uint32_t size = length; rt_uint8_t send_flg = 0; // 若为发送数据,则置MOSI为输出,若为接收数据,则置MOSI为输入,但这写法明显存在语法漏洞 if ((send_buf != RT_NULL) || (recv_buf == RT_NULL)) { MOSI_OUT(ops); send_flg = 1; } else { MOSI_IN(ops); } while (size--) { rt_uint8_t tx_data = 0xFF; rt_uint8_t rx_data = 0xFF; rt_uint8_t bit = 0; // 准备发送数据 if (send_buf != RT_NULL) { tx_data = *send_ptr++; } if (send_flg) { for (i = 0; i < 8; i++) { // 准备发送数据 if (config->mode & RT_SPI_MSB) { bit = tx_data & (0x1 << (7 - i)); } else { bit = tx_data & (0x1 << i); } // 发送数据 if (bit) MOSI_H(ops); else MOSI_L(ops); // 延时并翻转时钟 spi_delay2(ops); TOG_SCLK(ops); //延时并翻转时钟 spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 7)) { TOG_SCLK(ops); } } rx_data = tx_data; } else { for (i = 0; i < 8; i++) { // 延时并翻转clk spi_delay2(ops); TOG_SCLK(ops); //准备接收数据 if (config->mode & RT_SPI_MSB) { rx_data <<= 1; bit = 0x01; } else { rx_data >>= 1; bit = 0x80; } // 接收数据 if (GET_MOSI(ops)) { rx_data |= bit; } else { rx_data &= ~bit; } // 延时并翻转clk spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 7)) { TOG_SCLK(ops); } } } // 保存接收到的数据 if (recv_buf != RT_NULL) { *recv_ptr++ = rx_data; } } // 接收完毕,改为输出口 if (!send_flg) { MOSI_OUT(ops); } } return length; }
这实现,老实说,槽点太多,个人认为完全可以继续优化,初步修改的实现如下(未验证效果):
rt_inline rt_ssize_t spi_xfer_3line_data8(struct rt_spi_bit_ops *ops, struct rt_spi_configuration *config, const void *send_buf, void *recv_buf, rt_size_t length) { RT_ASSERT(ops != RT_NULL); RT_ASSERT(length != 0); rt_uint32_t size = length; int i = 0; if (send_buf != RT_NULL) { const rt_uint8_t *send_ptr = send_buf; rt_uint8_t tx_data = 0xFF; rt_uint8_t bit = 0; MOSI_OUT(ops); while (size--) { tx_data = *send_ptr++; for (i = 0; i < 8; i++) { if (config->mode & RT_SPI_MSB) { bit = tx_data & (0x1 << (7 - i)); } else { bit = tx_data & (0x1 << i); } if (bit) MOSI_H(ops); else MOSI_L(ops); spi_delay2(ops); TOG_SCLK(ops); spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 7)) { TOG_SCLK(ops); } } } } else if(recv_buf != RT_NULL) { rt_uint8_t *recv_ptr = recv_buf; rt_uint8_t rx_data = 0xFF; rt_uint8_t bit = (config->mode & RT_SPI_MSB) ? 0x01 : 0x80; MOSI_IN(ops); while (size--) { for (i = 0; i < 8; i++) { spi_delay2(ops); TOG_SCLK(ops); if (config->mode & RT_SPI_MSB) { rx_data <<= 1;} else { rx_data >>= 1;} if (GET_MOSI(ops)) { rx_data |= bit; } else { rx_data &= ~bit; } spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 7)) { TOG_SCLK(ops); } } *recv_ptr++ = rx_data; } MOSI_OUT(ops); } return length - size; }
spi_xfer_3line_data16
rt_inline rt_ssize_t spi_xfer_3line_data16(struct rt_spi_bit_ops *ops, struct rt_spi_configuration *config, const void *send_buf, void *recv_buf, rt_size_t length) { int i = 0; RT_ASSERT(ops != RT_NULL); RT_ASSERT(length != 0); { const rt_uint16_t *send_ptr = send_buf; rt_uint16_t *recv_ptr = recv_buf; rt_uint32_t size = length; rt_uint8_t send_flg = 0; if ((send_buf != RT_NULL) || (recv_buf == RT_NULL)) { MOSI_OUT(ops); send_flg = 1; } else { MOSI_IN(ops); } while (size--) { rt_uint16_t tx_data = 0xFFFF; rt_uint16_t rx_data = 0xFFFF; rt_uint16_t bit = 0; if (send_buf != RT_NULL) { tx_data = *send_ptr++; } if (send_flg) { for (i = 0; i < 16; i++) { if (config->mode & RT_SPI_MSB) { bit = tx_data & (0x1 << (15 - i)); } else { bit = tx_data & (0x1 << i); } if (bit) MOSI_H(ops); else MOSI_L(ops); spi_delay2(ops); TOG_SCLK(ops); spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 15)) { TOG_SCLK(ops); } } rx_data = tx_data; } else { for (i = 0; i < 16; i++) { spi_delay2(ops); TOG_SCLK(ops); if (config->mode & RT_SPI_MSB) { rx_data <<= 1; bit = 0x0001; } else { rx_data >>= 1; bit = 0x8000; } if (GET_MOSI(ops)) { rx_data |= bit; } else { rx_data &= ~bit; } spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 15)) { TOG_SCLK(ops); } } } if (recv_buf != RT_NULL) { *recv_ptr++ = rx_data; } } if (!send_flg) { MOSI_OUT(ops); } } return length; }
这代码如果不细看,很容易被误认为就是复制粘贴了8bit位宽的实现。而仔细看之后,会发现相比较于8bit以内的位宽,实际上这个的处理也仅仅是传输数据量多少的区别,以及接收和发送缓冲区按1字节处理还是2字节处理的区别。个人觉得有机会实现统一8bit和16bit的代码实现。
四线制SPI
相比较于三线制SPI,四线制SPI最大的特点是支持异步传输,虽然个人认为SPI的异步传输就是个伪命题(时钟和使能由master端控制,而这个时钟和使能还时有时无的,导致异步传输带来的好处并不能明显的表示出来,反而其劣势被放大了)。
同三线制初始化配置,若参数obj->config.mode未给RT_SPI_3WIRE这个配置,则代表此SPI总线工作在4线制模式,其对应的实现入口如下:
// spi_bit_xfer --> spi_xfer_4line_data8 // config->data_width <= 8 // --> spi_xfer_4line_data16 // 8 < config->data_width <= 16
spi_xfer_4line_data8
rt_inline rt_ssize_t spi_xfer_4line_data8(struct rt_spi_bit_ops *ops, struct rt_spi_configuration *config, const void *send_buf, void *recv_buf, rt_size_t length) { int i = 0; RT_ASSERT(ops != RT_NULL); RT_ASSERT(length != 0); { const rt_uint8_t *send_ptr = send_buf; rt_uint8_t *recv_ptr = recv_buf; rt_uint32_t size = length; while (size--) { rt_uint8_t tx_data = 0xFF; rt_uint8_t rx_data = 0xFF; rt_uint8_t bit = 0; // 获取下一个待发送的数据 if (send_buf != RT_NULL) { tx_data = *send_ptr++; } for (i = 0; i < 8; i++) { // 置发送标记 if (config->mode & RT_SPI_MSB) { bit = tx_data & (0x1 << (7 - i)); } else { bit = tx_data & (0x1 << i); } // 按照发送标记设置高低电平 if (bit) MOSI_H(ops); else MOSI_L(ops); // 时钟翻转 spi_delay2(ops); TOG_SCLK(ops); // 置读数据位 if (config->mode & RT_SPI_MSB) { rx_data <<= 1; bit = 0x01; } else { rx_data >>= 1; bit = 0x80; } // 读取接收端数据 if (GET_MISO(ops)) { rx_data |= bit; } else { rx_data &= ~bit; } // 延时并准备下一次接收 spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 7)) { TOG_SCLK(ops); } } // 保存接收到的数据 if (recv_buf != RT_NULL) { *recv_ptr++ = rx_data; } } } return length; }
相比较于三线制的实现,四线制的实现逻辑清晰了不少。
spi_xfer_4line_data16
rt_inline rt_ssize_t spi_xfer_4line_data16(struct rt_spi_bit_ops *ops, struct rt_spi_configuration *config, const void *send_buf, void *recv_buf, rt_size_t length) { int i = 0; RT_ASSERT(ops != RT_NULL); RT_ASSERT(length != 0); { const rt_uint16_t *send_ptr = send_buf; rt_uint16_t *recv_ptr = recv_buf; rt_uint32_t size = length; while (size--) { rt_uint16_t tx_data = 0xFFFF; rt_uint16_t rx_data = 0xFFFF; rt_uint16_t bit = 0; if (send_buf != RT_NULL) { tx_data = *send_ptr++; } for (i = 0; i < 16; i++) { if (config->mode & RT_SPI_MSB) { bit = tx_data & (0x1 << (15 - i)); } else { bit = tx_data & (0x1 << i); } if (bit) MOSI_H(ops); else MOSI_L(ops); spi_delay2(ops); TOG_SCLK(ops); if (config->mode & RT_SPI_MSB) { rx_data <<= 1; bit = 0x0001; } else { rx_data >>= 1; bit = 0x8000; } if (GET_MISO(ops)) { rx_data |= bit; } else { rx_data &= ~bit; } spi_delay2(ops); if (!(config->mode & RT_SPI_CPHA) || (size != 0) || (i < 15)) { TOG_SCLK(ops); } } if (recv_buf != RT_NULL) { *recv_ptr++ = rx_data; } } } return length; }
与三线制类似,其实四线制的16bit实现逻辑和8bit的实现逻辑差不多,唯一的区别是8bit传输变成了16bit传输,有机会做到8bit和16bit统一实现。
结论
分析完RTT的软件spi框架,我们可以发现相比较于软件i2c框架,RTT的软件spi还有巨大的改进空间。目前明显可以看出来可以改进的点如下:
a.所有依赖于硬件驱动的部分,完全可以做类似于软件i2c实现方法的优化,做到不需要在硬件层单独写drv_soft_spi.c接口
b.spi 的主频和延时的对应关系,完全可以更加精细化计算
c.数据位宽8bit和16bit的传输,是否有机会合并成一套实现?从实现上看,两个实现仅仅是传递bit数和传输变量类型的区别。
d.三线制的实现明显未考虑清楚,从接口实现上看,问题一堆:
i.三线制的特点决定了其只能实现单发送或单接收,因此实现上应只考虑单发送或单接收的情况
ii.变量的置位操作位置,明显未详细考量,实现位置不在最优位置上,如bit位的置位
iii.嵌套逻辑还可以继续优化
e.三线制的收发实现,有机会共用四线制实现,原因是从软件实现上看,实际上三线制实现可认为是读了后不用的四线制写以及写了之后不用的四线制读
虽然目前RTT集成的软件SPI框架代码有不少优化空间,但不可否认,这套框架确实能够实现gpio模拟SPI的功能,可以应用在硬件不支持spi的平台或spi接口数不够的项目中。