背景
此文用以复盘基于RT-Thread的SPIFI调试过程。由此总结相关知识,形成相应知识的经验总结。
硬件平台
MCU:LPC54606
NorFLash芯片:W25Q256
SPI接口:SPIFI
开始之前面临的问题
由于刚刚接触RT-Thread个把月左右的时间,SPIFI(甚至SPI)相关基础也不是太牢靠。在开始之前一直有几个疑惑。
SPIFI与SPI什么关系?
RT-Thread中只看到了SPI的驱动架构,没有看到SPIFI的,SPIFI怎么加入RT-Thread?
刚开始简直是一头雾水,看了官网关于SPI的相关文档,找了论坛里与SPI相关的讨论,均未找到明确的答案。
开始有头绪和线索,是从cychen的回帖开始的。在此非常感谢cychen的热心帮助,多次回帖甚至将自己已经完成的驱动分享给我。后来我才想到他也是最早在论坛里发帖讨论LPC54608下的SPIFI问题的人。在cycchen提示可参考STM32 QSPI以及分享给我的代码的提示下,现在已经搞明白了相关问题并调试通过。现在做个总结也分享给大家。
SPIFI与SPI什么关系?
SPIFI本质上就是SPI,只不过换了个名字和一堆NXP自己的SPIFI寄存器而已。
RT-Thread中只看到了SPI的驱动架构,没有看到SPIFI的,SPIFI怎么加入RT-Thread?
要将SPIFI将入SPI架构,需要将SPIFI的数据与QSPI的数据结构进行相互转换即可。下面将总结关键的数据结构与关键接口。理解了这些也就理解了如何进行驱动挂接。
关键数据结构
spifi_command_t
1/*! @brief SPIFI command structure */
2typedef struct _spifi_command
3{
4 uint16_t dataLen; /*!< How many data bytes are needed in this command. */
5 bool isPollMode; /*!< For command need to read data from serial flash */
6 spifi_data_direction_t direction; /*!< Data direction of this command. */
7 uint8_t intermediateBytes; /*!< How many intermediate bytes needed */
8 spifi_command_format_t format; /*!< Command format */
9 spifi_command_type_t type; /*!< Command type */
10 uint8_t opcode; /*!< Command opcode value */
11} spifi_command_t;
该结构体是在NXP提供的官方驱动中的数据结构,该结构根据SPIFI的command register来构造。用来通知MCU当前发送的SPI指令的相关参数。
rt_qspi_message
1struct rt_qspi_message
2{
3 struct rt_spi_message parent;
4 /* instruction stage */
5 struct
6 {
7 rt_uint8_t content;
8 rt_uint8_t qspi_lines;
9 } instruction;
10 /* address and alternate_bytes stage */
11 struct
12 {
13 rt_uint32_t content;
14 rt_uint8_t size;
15 rt_uint8_t qspi_lines;
16 } address, alternate_bytes;
17 /* dummy_cycles stage */
18 rt_uint32_t dummy_cycles;
19 /* number of lines in qspi data stage, the other configuration items are in parent */
20 rt_uint8_t qspi_data_lines;
21};
该数据结构为RT-Thread用于设置SPI相关指令参数的数据结构。可以看出该数据结构继承自rt_spi_message
。在此基础上又定义了QSPI特有的参数,包括指令结构(指令内容/数据宽度),地址结构(地址内容/长度/数据宽度),空时钟数量,收发数据的宽度。
该数据结构是RT-Thread对SPI指令进行的抽象,我们在不用的硬件平台上实现驱动时就是要将这个抽象的数据结构转化为具体硬件平台的数据结构(不同芯片厂商所提供驱动的数据结构),然后交由芯片驱动进行操作。这应当是RT-Thread 驱动的关键。
转化方法
1void spifi_send_cmd(SPIFI_Type *base, struct rt_qspi_message *message)
2{
3 spifi_command_t spifi_command;
4 RT_ASSERT(message != RT_NULL);
5 /*发送或接收的数据长度*/
6 spifi_command.dataLen = message->parent.length;
7 spifi_command.isPollMode = false;
8
9 /*根据message中的rcv_buf/send_buf填写dirction*/
10 if (message->parent.recv_buf)
11 spifi_command.direction = kSPIFI_DataInput;
12 else
13 spifi_command.direction = kSPIFI_DataOutput;
14
15 /*
16 根据message中的instruction.qspi_lines/address.qspi_lines/message->qspi_data_lines
17 完成对spifi_command.format的设置
18 PS:完成对spifi_command下的format是对指令、地址、数据传输时使用的数据宽度进行设置
19 */
20 /*
21 instruction/address/qspi_data_lines均不为2(dual双线模式)或4(quad四线模式)
22 指令、地址、数据传输时均使用单线制
23 */
24 if (message->instruction.qspi_lines != 2 &&
25 message->instruction.qspi_lines != 4 &&
26 message->address.qspi_lines != 2 &&
27 message->address.qspi_lines != 4 &&
28 message->qspi_data_lines != 2 &&
29 message->qspi_data_lines != 4)
30 {
31 spifi_command.format = kSPIFI_CommandAllSerial;
32 }
33 else if (message->qspi_data_lines == 4 &&
34 message->address.qspi_lines != 4 &&
35 message->instruction.qspi_lines != 4)
36 {/*仅date使用quad模式*/
37 spifi_command.format = kSPIFI_CommandDataQuad;
38 }
39 else if (message->instruction.qspi_lines == 1 &&
40 message->address.qspi_lines != 1 &&
41 message->qspi_data_lines != 1)
42 {/*仅指令部分使用串行模式*/
43 spifi_command.format = kSPIFI_CommandOpcodeSerial;
44 }
45 else
46 {/*所有均使用quad模式*/
47 spifi_command.format = kSPIFI_CommandAllQuad;
48 }
49 /*
50 intermediateBytes对应着dummy cycles,
51 但是注意intermediateBytes的单位是byte
52 dummy cycles单位是bit
53 同时,注意他们之间的关系不能单纯的使用dummy_cycles / 8
54 对于dual或者quad模式应当使用dummy_cycles*message->address.qspi_lines / 8;
55 否则将导致dual或者quad模式下的读写错误
56 */
57 if(message->address.qspi_lines!=0)
58 spifi_command.intermediateBytes = message->dummy_cycles*message->address.qspi_lines / 8;
59 else
60 spifi_command.intermediateBytes = message->dummy_cycles / 8;
61 /*命令*/
62 spifi_command.opcode = message->instruction.content;
63
64 /*
65 根据message中指令和地址的线宽,设置spifi中的type
66 */
67 if (message->instruction.qspi_lines != 0)
68 {
69 if (message->address.qspi_lines == 0)
70 spifi_command.type = kSPIFI_CommandOpcodeOnly;
71 else if (message->address.size == 8)
72 spifi_command.type = kSPIFI_CommandOpcodeAddrOneByte;
73 else if (message->address.size == 16)
74 spifi_command.type = kSPIFI_CommandOpcodeAddrTwoBytes;
75 else if (message->address.size == 24)
76 spifi_command.type = kSPIFI_CommandOpcodeAddrThreeBytes;
77 else
78 spifi_command.type = kSPIFI_CommandOpcodeAddrFourBytes;
79 } else {
80 if (message->address.size == 24)
81 spifi_command.type = kSPIFI_CommandNoOpcodeAddrThreeBytes;
82 else if (message->address.size == 32)
83 spifi_command.type = kSPIFI_CommandNoOpcodeAddrFourBytes;
84 }
85 if (message->address.qspi_lines)
86 SPIFI_SetCommandAddress(base, message->address.content);
87 SPIFI_SetCommand(base, &spifi_command);
88}
这部分不再多说,注意以上代码中的注释就好,已经做了详细说明。
RT-Thread数据结构思想
这部分与SPI无关,是SPI实现中的副产品,仅仅是我在实现SPI这部分功能中对RT-Thread数据结构的一点理解,记录于此。其实这部分在RT-Thread的官方文档中已经有说明,只是接触个把月来,到现在才有较深刻的认识,所以有必要在此记录一下,以帮助自己和他人理解RT-Thread的世界观。
RT-Thread的虽然是C语言的,但是其数据结构的设计大量的使用了面向对象的思想。比如rt_object
,这是整个RT-Thread最基础的一个类,在rt-object的基础上,派生出了大量其他类。
通过上面的图我们看到,RT-Thread中,将所有用于进程间同步与通信的数据结构抽象出了一个ipc结构,在这个结构的基础上,再派生出了sem/mutex/event/mb/mq
而上面的数据结构中,在我们在驱动实现中,最多使用到的是rt_device。
拿spi举例,有一个基本的数据结构rt_qspi_device:
该数据结构定义如下:
1struct rt_qspi_device
2{
3 struct rt_spi_device parent; /*继承自rt_spi_device*/
4 struct rt_qspi_configuration config;
5 void (*enter_qspi_mode)(struct rt_qspi_device *device);
6 void (*exit_qspi_mode)(struct rt_qspi_device *device);
7};
从数据结构定义可以看出该数据结构继承自rt_spi_device。
再看rt_spi_device
1/**
2 * SPI Virtual BUS, one device must connected to a virtual BUS
3 */
4struct rt_spi_device
5{
6 struct rt_device parent;
7 struct rt_spi_bus *bus;
8 struct rt_spi_configuration config;
9 void *user_data;
10};
这里我们看出该数据结构作为一个device继承了rt_device的一些特性,同时,这个device挂载在spi总线上,其也包含了rt_spi_bus的特性;
这里的user_data,是用来让我们挂载不同芯片特性的,比如不同芯片的总线操作基地址,总线配置等等。
在我们的代码里,这个指针指向了lpc_spifi
1struct lpc_spifi
2{
3 SPIFI_Type *base; /*SPIFI寄存器基地址*/
4 struct rt_qspi_configuration *cfg;
5};
明白了这些,我们就能够明白RT-Thread是如何组织不同的数据结构,不同的驱动结构,以及如何将不同的芯片特有操作集成到一个统一的驱动框架下的。
关键接口
在实现了关键数据结构以后,对于SPI只需要实现两个基本API就可以了。
1static struct rt_spi_ops lpc_spifi_ops =
2{
3 configure,
4 spixfer
5};
即实现SPI的配置与数据传输。
spixfer是所有数据传输的接口,任何的数据传输都需要最后都需要使用这个接口。
这里有几个经验总结一下:
当我们调试过程中遇到问题,总是调试不通的时候,可以使用官方例程或者我们在其他地方跑通的程序放到configure中进行测试。
比如本次调试中,我一直怀疑初始化部分有问题,于是就将例程直接放入configure中进行调试,直接发现例程的配置是可以运行起来的,在此基础上,再将例程进行修改优化即可。在实现本部分时,一直有个疑惑,下面这个ops的数据结构中,两个接口的第二个参数是rt_spi_configuration。但我要传入的参数是rt_qspi_configuration,当时一直都想不明白如何传入该参数,毕竟这个ops是rtt定义好的,我不能修改他。
1/**
2 * SPI operators
3 */
4struct rt_spi_ops
5{
6 rt_err_t (*configure)(struct rt_spi_device *device, struct rt_spi_configuration *configuration);
7 rt_uint32_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);
8};
后来看到STM32 QSPI的实现代码,明白了如何实现。实现代码如下:
1static rt_uint32_t spixfer(struct rt_spi_device *device, struct rt_spi_message *message)
2{
3 uint8_t val;
4 rt_size_t i;
5 rt_size_t len = 0;
6 RT_ASSERT(device != RT_NULL);
7 RT_ASSERT(device->bus != RT_NULL);
8 RT_ASSERT(device->bus->parent.user_data != RT_NULL);
9 struct lpc_spifi *spifi = (struct lpc_spifi *)(device->bus->parent.user_data);
10 struct lpc_sw_spifi_cs *cs = device->parent.user_data;
11 struct rt_qspi_message *qspi_message = (struct rt_qspi_message *)message;
12 if(message->cs_take)
13 {
14
15 }
16
17 const rt_uint8_t *sndb = message->send_buf;
18 rt_uint8_t *rcvb = message->recv_buf;
19 rt_size_t length = message->length;
20 /* send data */
21 if (sndb)
22 {
23 spifi_send_cmd(spifi->base, qspi_message);
24 if(qspi_message->parent.length != 0)
25 {
26 for (i = 0; i < length; i++)
27 {
28 SPIFI_WriteDataByte(spifi->base, *sndb);
29 sndb++;
30 }
31 len = length;
32 }
33 else
34 {
35 len = 1;
36 }
37 }
38 else if (rcvb)
39 {
40 spifi_send_cmd(spifi->base, qspi_message);
41
42 for (i = 0; i < length; i++)
43 {
44 val = SPIFI_ReadDataByte(spifi->base);
45 *rcvb = val;
46 rcvb++;
47 }
48 len = length;
49 }
50 if (message->cs_release)
51 {
52 }
53 return len;54 }
我们传入的是一个指针,这个指针是可以进行类型转换的,使用指针的类型转换,可以有效的进行数据结构的切换。
当然,这对我们定义的数据结构也是有要求的,即我们上面提到的面向对象的定义方法,且数据结构的公共部分一定是放在数据结构的起始的。
总结
以上,就是本次开发的一些经验总结。感谢大家的阅读!
RT-Thread 近期活动
现在报名即享三重福利,1、报名即可以领取往期能力认证培训资料大礼包 2、参与新能力认证考前培训 3、学生优惠价168元
扫码报名
企业人才服务:
在这个特殊时期,针对人才服务RT-Thread将开放更多的资源,如果你的企业在招聘嵌入式相关人才,可提交职位信息说明,RT-Thread将会通过微信公众号免费发布相关职位信息(要求RT-Thread技能的职位优先发布)
请详细登记您的职位信息,我们将尽快联系你确认岗位描述信息,安排发布
扫码即可提交需求
你可以添加微信17775982065为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群!
RT-Thread
让物联网终端的开发变得简单、快速,芯片的价值得到最大化发挥。Apache2.0协议,可免费在商业产品中使用,不需要公布源码,无潜在商业风险。
长按二维码,关注我们
点击阅读原文,进入RT-Thread GitHub首页
你点的每个“在看”,我都认真当成了喜欢