K230-SPI驱动移植

SPI协议简述

标准SPI

SPI(Serial Peripheral Interface)是一种高速、全双工的同步串行通信协议,广泛应用于微控制器与外设(如Flash、传感器、显示屏)之间的通信。

标准SPI时序如下(CPOL = 1,CPHA = 0):

标准SPI时序图

注:图中没有标出如下的建立/保持时间,这些详细时序要求可以参考芯片手册。

  • tCSS: CS建立时间(CS下降沿到第一个时钟上升沿)
  • tCSH: CS保持时间(最后时钟下降沿到CS上升沿)
  • tSU: 数据建立时间(数据稳定到时钟采样沿)
  • tH: 数据保持时间(时钟采样沿后数据保持稳定)

SPI有4种工作模式,由两个参数决定:

  • CPOL(时钟极性):空闲时时钟线的电平(0=低电平,1=高电平)
  • CPHA(时钟相位):数据采样时刻(0=第一个边沿,1=第二个边沿)
模式 CPOL CPHA 空闲时钟 采样边沿 输出边沿
0 0 0 低电平 上升沿 下降沿
1 0 1 低电平 下降沿 上升沿
2 1 0 高电平 下降沿 上升沿
3 1 1 高电平 上升沿 下降沿

模式0和模式3最常用,大多数SPI设备支持其中之一。主从设备必须使用相同的模式才能正常通信。

详细时序如下(时钟建立时间不准)

SPI工作模式

标准SPI虽然简单可靠,但在现代应用中面临明显的带宽瓶颈。由于采用单线传输方式,MOSI和MISO各占一根线进行半双工数据传输,即使时钟频率达到50MHz,单线传输速率也仅有6.25MB/s。这在大容量Flash应用中尤为突出:从128MB Flash读取1MB数据需要约160ms,系统启动时加载数MB固件会产生明显延迟,而实时应用(如视频、图像处理)更是无法满足带宽需求。

增强型SPI

一个最简单的办法是增加数据线线,让传输并行化,因此诞生了Dual SPI(双线)、Quad SPI(四线)以及Octal SPI(8线)。标准SPI采用全双工设计,MOSI和MISO是独立的单向线路,可同时收发数据。而DSPI/QSPI/OSPI采用半双工设计,数据线是双向复用的,同一时刻只能进行读或写操作。这种设计牺牲了全双工能力,但通过增加并行数据线大幅提升了单向传输带宽。

三种增强型SPI协议传输可以分成三个阶段:命令、地址、数据,以QSPI为例,其传输时序为:

QSPI发送模式

图中,命令为4bit,单线传输;地址和数据都是8bit,4线传输。实际上,命令线和地址线都可以用1/2/4/8线传输(也就是向下兼容),或者省略,进行数据阶段的传输。

硬件平台

从K230芯片架构可以看出,K230有一个Octal SPI控制器(OPI)和两个Quad SPI控制器(QPI)。

K230芯片架构

对于OPI,其支持1/2/4/8线传输模式,最大时钟频率为200MHz。而QPI控制器支持1/2/4线传输模式,最大频率为100MHz。

需要注意的是,K230 的SPI控制器内部集成了一套自己的DMA(手册里叫Internal DMA),用来直接在 SPI 与内存之间搬运数据。它和系统外设DMA控制器PDMA是两套独立的硬件,互不复用通道,所以SPI的DMA传输不需要走统一的PDMA框架,对PDMA驱动也没有依赖。

驱动架构设计

配置部分

对于RT-smart而言,其内核中有两个和SPI相关的组件:SPI和QSPI,SPI的基础配置结构体rt_spi_configuration定义了标准SPI的核心参数,而QSPI配置结构体rt_qspi_configuration在标准SPI基础上扩展了多线传输能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @brief SPI configuration structure
*/
struct rt_spi_configuration
{
rt_uint8_t mode;
rt_uint8_t data_width;
#ifdef RT_USING_DM
rt_uint8_t data_width_tx;
rt_uint8_t data_width_rx;
#else
rt_uint16_t reserved;
#endif
rt_uint32_t max_hz;
};
/**
* @brief QSPI configuration structure
*/
struct rt_qspi_configuration
{
struct rt_spi_configuration parent;
/* The size of medium */
rt_uint32_t medium_size;
/* double data rate mode */
rt_uint8_t ddr_mode;
/* the data lines max width which QSPI bus supported, such as 1, 2, 4 */
rt_uint8_t qspi_dl_width ;
};

这种继承设计使得QSPI配置向下兼容标准SPI,同时qspi_dl_width支持8线参数,因此K230的所有SPI控制器(包括OPI和QPI)都可以注册为QSPI总线。

在RT-smart中,QSPI和SPI设备共用同一套操作接口rt_spi_ops

1
2
3
4
5
6
7
8
/**
* @brief SPI operators
*/
struct rt_spi_ops
{
rt_err_t (*configure)(struct rt_spi_device *device, struct rt_spi_configuration *configuration);
rt_ssize_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);
};

驱动开发的核心工作就是实现这两个函数指针:

  • configure:根据配置参数初始化硬件寄存器
  • xfer:执行实际的数据传输操作

由于QSPI和标准SPI兼容同一套接口,rt_spi_ops内部两个虚函数参数用的是rt_spi_devicert_spi_configurationrt_spi_message。因此,驱动层需要利用rt_qspi_device->parentrt_spi_device首地址相同的特性进行强制转换:

1
2
3
4
5
6
7
8
9
10
11
12
rt_err_t drv_spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *configuration)
{
struct rt_qspi_device *qspi_device = (struct rt_qspi_device *)device;
struct rt_qspi_configuration *qspi_cfg = &qspi_device->config;
// ...
}
rt_ssize_t drv_spi_xfer(struct rt_spi_device *device, struct rt_spi_message *message)
{
struct rt_qspi_device *qspi_device = (struct rt_qspi_device *)device;
struct rt_qspi_message *qspi_msg = (struct rt_qspi_message *)message;
// ...
}

下面看看应用层调用QSPI总线的流程,需要做哪些配置,使用了哪些函数,以及用户的参数是如何从应用层传递到驱动层的ops吧。详细代码参考:K230 SPI utest

抛开GPIO的功能配置以及输入输出模式选择,首先需要找到已注册的SPI总线,然后将设备挂载到总线上。

1
2
3
4
5
/* Find QSPI Bus */
struct rt_spi_bus *spi_bus = (struct rt_spi_bus *)rt_device_find(SPI0_BUS_NAME);
qspi_dev = (struct rt_qspi_device *)rt_malloc(sizeof(struct rt_qspi_device));
/* Attach SPI Device */
ret = rt_spi_bus_attach_device(&(qspi_dev->parent), SPI0_DEV_NAME0, SPI0_BUS_NAME, RT_NULL);

由于RT-smart不支持直接访问SPI总线,因此应用层必须绑定设备,以设备的形式进行总线操作。这种设计允许一条总线上挂载多个设备,每个设备可以有独立的配置参数。

接下来是创建配置结构体并设置参数,然后调用配置函数:

1
2
3
4
5
6
7
8
9
10
11
/* SPI Device Config*/
struct rt_qspi_configuration qspi_cfg;
qspi_cfg.parent.mode = RT_SPI_MODE_0 | RT_SPI_MSB;
qspi_cfg.parent.data_width = 8;
qspi_cfg.parent.max_hz = 1000000;
qspi_cfg.parent.reserved = 0;
qspi_cfg.qspi_dl_width = 1;
qspi_cfg.medium_size = 0;
qspi_cfg.ddr_mode = 0;

ret = rt_qspi_configure(qspi_dev, &qspi_cfg);

核心是rt_qspi_configure这个函数,它的实现在dev_qspi_core.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
rt_err_t rt_qspi_configure(struct rt_qspi_device *device, struct rt_qspi_configuration *cfg)
{
RT_ASSERT(device != RT_NULL);
RT_ASSERT(cfg != RT_NULL);

/* reset the CS pin */
...............................
/* If the configurations are the same, we don't need to set again. */
if (device->config.medium_size == cfg->medium_size &&
device->config.ddr_mode == cfg->ddr_mode &&
device->config.qspi_dl_width == cfg->qspi_dl_width &&
device->config.parent.data_width == cfg->parent.data_width &&
device->config.parent.mode == (cfg->parent.mode & RT_SPI_MODE_MASK) &&
device->config.parent.max_hz == cfg->parent.max_hz)
{
return RT_EOK;
}
/* copy configuration items */
device->config.parent.mode = cfg->parent.mode;
device->config.parent.max_hz = cfg->parent.max_hz;
device->config.parent.data_width = cfg->parent.data_width;
#ifdef RT_USING_DM
device->config.parent.data_width_tx = cfg->parent.data_width_tx;
device->config.parent.data_width_rx = cfg->parent.data_width_rx;
#else
device->config.parent.reserved = cfg->parent.reserved;
#endif
device->config.medium_size = cfg->medium_size;
device->config.ddr_mode = cfg->ddr_mode;
device->config.qspi_dl_width = cfg->qspi_dl_width;

return rt_spi_bus_configure(&device->parent);
}

上面的代码我省略了软件CS这一部分,rt_qspi_configure中,对比了rt_qspi_device->configrt_qspi_configuration的新旧版配置,避免重复配置硬件,将用户配置保存到device->config中,调用底层总线配置函数

rt_qspi_device的定义展示了设备、配置和总线的关系:

1
2
3
4
5
6
7
8
9
10
/**
* @brief QSPI operators
*/
struct rt_qspi_device
{
struct rt_spi_device parent;
struct rt_qspi_configuration config;
void (*enter_qspi_mode)(struct rt_qspi_device *device);
void (*exit_qspi_mode)(struct rt_qspi_device *device);
};

rt_qspi_device的父类是rt_spi_device,而它自己也有一个qspi配置结构体rt_qspi_configuration,再看rt_spi_bus_configure这个函数,它的实现在:dev_spi_core.c,这是配置流程的最后一环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rt_err_t rt_spi_bus_configure(struct rt_spi_device *device)
{
rt_err_t result = -RT_ERROR;
if (device->bus != RT_NULL){
result = spi_lock(device->bus);
if (result == RT_EOK){
if (device->bus->owner == device){
result = device->bus->ops->configure(device, &device->config);
if (result != RT_EOK){
LOG_E("SPI device %s configuration failed", device->parent.parent.name);
}
}else{
result = -RT_EBUSY;
}
spi_unlock(device->bus);
}
}else{
result = RT_EOK;
}
return result;
}

当确认获取锁后,rt_spi_bus_configure会把配置部分传递给ops结构体:device->bus->ops->configure(device, &device->config)

这里存在一个需要注意的问题:rt_spi_bus_configure接收的device参数类型是rt_spi_device(即rt_qspi_device->parent),因此传递给驱动层的配置参数&device->config实际指向rt_qspi_device->parent.config,而不是rt_qspi_device->config.parent

这意味着驱动层的 configure 函数收到的第二个参数 configuration 其实并不是应用层程序在 rt_qspi_configure 里设置的那份配置。因此驱动实现时没法使用这个参数,而是要先把 device 强转回 rt_qspi_device,再从 qspi_device->config(里取真正的配置:

1
2
3
4
5
6
7
rt_err_t drv_spi_configure(struct rt_spi_device *device, struct rt_spi_configuration *configuration)
{
struct rt_qspi_device *qspi_device = (struct rt_qspi_device *)device;
struct rt_qspi_configuration *qspi_cfg = &qspi_device->config; // 正确的配置

// ...
}

传输部分

QSPI消息结构体rt_qspi_message完整描述了传输信息的所有参数。其定义位于:dev_spi.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct rt_qspi_message
{
struct rt_spi_message parent;
/* instruction stage */
struct
{
rt_uint8_t content;
rt_uint8_t qspi_lines;
} instruction;
/* address and alternate_bytes stage */
struct
{
rt_uint32_t content;
rt_uint8_t size;
rt_uint8_t qspi_lines;
} address, alternate_bytes;
/* dummy_cycles stage */
rt_uint32_t dummy_cycles;
/* number of lines in qspi data stage, the other configuration items are in parent */
rt_uint8_t qspi_data_lines;
};

这个结构体继承自标准SPI消息rt_spi_message

1
2
3
4
5
6
7
8
9
10
struct rt_spi_message
{
const void *send_buf;
void *recv_buf;
rt_size_t length;
struct rt_spi_message *next;

unsigned cs_take : 1;
unsigned cs_release : 1;
};

rt_qspi_message对应增强型SPI的三个传输阶段:

  • instruction:命令阶段,包含命令内容和线数配置
  • address:地址阶段,包含地址内容、长度和线数配置
  • parent:数据阶段,继承自rt_spi_message,包含发送/接收缓冲区和长度

每个阶段都可以独立配置线数(1/2/4/8),实现灵活的传输组合。例如下面的配置实际是把QSPI当标准SPI使用:命令和地址阶段都省略(size=0),只进行数据阶段的单线传输。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Create SPI Message */
struct rt_qspi_message msg;
rt_memset(&msg, 0, sizeof(msg));
/*Using Standard SPI*/
msg.instruction.content = 0;
msg.instruction.qspi_lines = 1;
msg.address.content = 0;
msg.address.size = 0;
msg.address.qspi_lines = 1;
msg.qspi_data_lines = 1;
msg.dummy_cycles = 0;

/* SPI Message Config */
msg.parent.send_buf = tx_data;
msg.parent.recv_buf = rx_data;
msg.parent.length = TEST_DATA_LENGTH;
msg.parent.cs_take = 1;
msg.parent.cs_release = 1;
msg.parent.next = RT_NULL;

/* Transfer Data */
ret = rt_qspi_transfer_message(qspi_dev, &msg);

rt_qspi_transfer_message函数是传输流程的入口,其实现展示了内核如何处理总线共享和设备切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
rt_ssize_t rt_qspi_transfer_message(struct rt_qspi_device  *device, struct rt_qspi_message *message)
{
rt_ssize_t result;
RT_ASSERT(device != RT_NULL);
RT_ASSERT(message != RT_NULL);
result = rt_mutex_take(&(device->parent.bus->lock), RT_WAITING_FOREVER);
if (result != RT_EOK){
rt_set_errno(-RT_EBUSY);
return 0;
}
rt_set_errno(RT_EOK);
/* configure SPI bus */
if (device->parent.bus->owner != &device->parent){
/* not the same owner as current, re-configure SPI bus */
result = device->parent.bus->ops->configure(&device->parent, &device->parent.config);
if (result == RT_EOK){
device->parent.bus->owner = &device->parent;
}else{
rt_set_errno(-RT_EIO);
goto __exit;
}
}
result = device->parent.bus->ops->xfer(&device->parent, &message->parent);
if (result == 0){
rt_set_errno(-RT_EIO);
}
__exit:
rt_mutex_release(&(device->parent.bus->lock));
return result;
}

SPI总线经常遇到多设备共享的情况。除了互斥锁,RT-smart还通过总线所有权机制来处理:对比当前总线拥有者是否是当前设备,如果不是则重新配置device->parent.bus->ops->configure(&device->parent, &device->parent.config)。这样不同配置的设备可以共享同一条总线。配置完成后,调用驱动层ops传输数据:device->parent.bus->ops->xfer(&device->parent, &message->parent)

实现细节

之前提到,驱动层除了中断注册,设备注册等,其核心就是实现rt_spi_ops这个结构体中的虚函数,也就是总线的操作方法:

1
2
3
4
5
static const struct rt_spi_ops k230_qspi_ops =
{
.configure = k230_spi_configure,
.xfer = k230_spi_xfer,
};

在驱动中,我们需要实现初始化rt_hw_qspi_bus_init,配置k230_spi_configure,传输k230_spi_xfer以及中断回调函数k230_spi_irq,驱动的详细代码参考:K230 SPI驱动

初始化

驱动的初始化发生在内核启动的过程中,在用户态启用之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int rt_hw_qspi_bus_init(void)
{
rt_err_t ret;
int i;
for (i = 0; i < sizeof(k230_spi_devs) / sizeof(k230_spi_devs[0]); i++)
{
k230_spi_devs[i].base = rt_ioremap((void *)k230_spi_devs[i].pa_base, k230_spi_devs[i].size);
ret = rt_qspi_bus_register(&k230_spi_devs[i].dev, k230_spi_devs[i].name, &k230_qspi_ops);
if (ret)
{
LOG_E("%s register fail", k230_spi_devs[i].name);
return ret;
}
rt_event_init(&k230_spi_devs[i].event, k230_spi_devs[i].event_name, RT_IPC_FLAG_PRIO);
rt_hw_interrupt_install(k230_spi_devs[i].vector + SSI_TXE, k230_spi_irq, &k230_spi_devs[i], k230_spi_devs[i].name);
rt_hw_interrupt_umask(k230_spi_devs[i].vector + SSI_TXE);
rt_hw_interrupt_install(k230_spi_devs[i].vector + SSI_RXF, k230_spi_irq, &k230_spi_devs[i], k230_spi_devs[i].name);
rt_hw_interrupt_umask(k230_spi_devs[i].vector + SSI_RXF);
rt_hw_interrupt_install(k230_spi_devs[i].vector + SSI_DONE, k230_spi_irq, &k230_spi_devs[i], k230_spi_devs[i].name);
rt_hw_interrupt_umask(k230_spi_devs[i].vector + SSI_DONE);
rt_hw_interrupt_install(k230_spi_devs[i].vector + SSI_AXIE, k230_spi_irq, &k230_spi_devs[i], k230_spi_devs[i].name);
rt_hw_interrupt_umask(k230_spi_devs[i].vector + SSI_AXIE);
}
return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_qspi_bus_init);

驱动初始化在内核启动时完成,通过INIT_DEVICE_EXPORT,实现主要工作包括:

  • 寄存器映射:通过rt_ioremap将物理地址映射到虚拟地址空间,k230_spi_devs[i].base被内存映射IO指向SPI相关的寄存器,也就是从k230_spi_devs[i].pa_base开始,向高位偏移k230_spi_devs[i].size字节,具体Memory Map参数参考board.h

  • 总线注册:调用rt_qspi_bus_register注册QSPI总线。

  • 中断注册:注册发送空(SSI_TXE)、接收满(SSI_RXF)、传输完成(SSI_DONE)和AXI错误(SSI_AXIE)四个中断。

配置

配置函数k230_spi_configure首先进行参数校验,确保线数不超过硬件限制(OPI最大8线,QPI最大4线),数据宽度在4-32位范围内,时钟频率不超过硬件最大值。

然后是配置时钟,先获取SPI控制器的时钟频率,然后计算分频系数:

1
2
3
4
5
6
7
if (qspi_bus->idx == 0){
qspi_clk = sysctl_clk_get_leaf_freq(SYSCTL_CLK_SSI0);
}else if (qspi_bus->idx == 1){
qspi_clk = sysctl_clk_get_leaf_freq(SYSCTL_CLK_SSI1);
}else if (qspi_bus->idx == 2){
qspi_clk = sysctl_clk_get_leaf_freq(SYSCTL_CLK_SSI2);
}

接下来是将应用层的配置参数映射到硬件寄存器。其中,qspi_dl_width决定了SPI传输模式(标准/双线/四线/八线),mode包含CPOL和CPHA配置,data_width对应数据帧大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (qspi_cfg->qspi_dl_width == 1){
spi_ff = SPI_FRF_STD_SPI;
}else if (qspi_cfg->qspi_dl_width == 2){
spi_ff = SPI_FRF_DUAL_SPI;
}else if (qspi_cfg->qspi_dl_width == 4){
spi_ff = SPI_FRF_QUAD_SPI;
}else if (qspi_cfg->qspi_dl_width == 8) {
spi_ff = SPI_FRF_OCT_SPI;
}else{
return -RT_EINVAL;
}
mode = qspi_cfg_parent->mode & RT_SPI_MODE_3;
dfs = qspi_cfg_parent->data_width - 1;
qspi_reg->ssienr = 0; // 禁用控制器
qspi_reg->ser = 0; // 禁用片选
qspi_reg->baudr = qspi_clk / max_hz; // 时钟分频
qspi_reg->rx_sample_delay = qspi_bus->rdse << 16 | qspi_bus->rdsd; // 采样延迟
qspi_reg->axiawlen = SSIC_AXI_BLW << 8; // AXI写突发长度
qspi_reg->axiarlen = SSIC_AXI_BLW << 8; // AXI读突发长度
qspi_reg->ctrlr0 = (dfs) | (mode << 8) | (spi_ff << 22); // 控制寄存器

这里涉及的寄存器包括:ssienr(使能控制)、ser(片选)、baudr(波特率)、rx_sample_delay(接收采样延迟)、axiawlenaxiarlen(AXI突发长度)、ctrlr0(主控制寄存器)。详细的寄存器定义参考K230技术参考手册

传输

传输函数k230_spi_xfer根据传输模式分为两种模式:标准单线SPI和增强型多线SPI。函数首先通过判断qspi_data_lines来决定使用哪种传输方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rt_ssize_t k230_spi_xfer(struct rt_spi_device *device, struct rt_spi_message *message)
{
struct k230_spi_dev *qspi_bus = (struct k230_spi_dev *)device->bus;
k230_spi_reg_t *qspi_reg = (k230_spi_reg_t *)qspi_bus->base;
struct rt_qspi_device *dev = (struct rt_qspi_device *)device;
struct rt_qspi_configuration *qspi_cfg = &dev->config;
struct rt_spi_configuration *qspi_cfg_parent = &dev->config.parent;
struct rt_qspi_message *msg = (struct rt_qspi_message *)message;
struct rt_spi_message *msg_parent = message;

/* 判断是否为增强型SPI传输 */
if (msg->qspi_data_lines > 1)
{
// ...
}else {

// ...
}
}

增强型SPI传输

增强型SPI传输需要更复杂的参数校验和配置。代码首先验证传输参数的合法性,包括数据线数不能超过硬件配置的最大值,数据宽度必须与线数匹配等等。

传输类型trans_type用于配置spi_ctrlr0寄存器的低2位,决定指令和地址阶段的传输模式。当指令使用多线传输时设置为2,当地址使用多线传输时设置为1。如果指令和地址都使用多线,则保持trans_type为2。

1
2
3
4
5
6
7
8
9
10
11
if (msg->instruction.qspi_lines != 1){
trans_type = 2;
}
if (msg->address.size){
if (msg->address.qspi_lines != 1){
trans_type = trans_type ? trans_type : 1;
}else if (trans_type != 0){
LOG_E("instruction or address line is invalid");
return 0;
}
}

增强型SPI一般涉及到大量数据的收发,驱动采用了DMA传输模式。这里的DMA是SPI控制器内部的Internal DMA,并不是SoC的PDMA控制器,两者在硬件上完全独立,因此SPI的DMA只需要配置本控制器的寄存器,不用申请PDMA通道。

由于这个DMA直接访问物理内存,DMA传输需要使用cache对齐的缓冲区。对于发送操作,驱动将用户数据复制到对齐缓冲区并执行cache清理操作,确保数据写入内存;对于接收操作,传输完成后需要使cache无效,然后将数据复制回用户缓冲区。这种设计避免了cache一致性问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rt_uint8_t tmod = msg_parent->recv_buf ? SPI_TMOD_RO : SPI_TMOD_TO;
rt_size_t length = msg_parent->length;
rt_size_t txfthr = length > (SSIC_TX_ABW / 2) ? (SSIC_TX_ABW / 2) : length - 1;
rt_uint8_t cell_size = (qspi_cfg_parent->data_width + 7) >> 3;
rt_uint8_t *buf = RT_NULL;

/* 分配cache对齐的DMA缓冲区 */
if (length){
buf = rt_malloc_align(CACHE_ALIGN_TOP(length * cell_size), L1_CACHE_BYTES);
if (buf == RT_NULL){
LOG_E("alloc mem error");
return 0;
}
}

寄存器配置包括传输参数和DMA设置。spi_ctrlr0的低2位是传输类型,第5-2位是地址长度(向下对齐到4字节边界),第10-8位为0x2只读,第15-11位是等待周期。FIFO阈值根据传输长度动态调整,发送阈值设置为传输长度和FIFO深度一半的较小值,接收阈值设置为FIFO深度减1,之后取消SSI_DONE和SSI_AXIE中断的掩码,最后使能DMA。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* msg->address.size & ~0x03:  Round address.size down to the nearest multiple of 4 and write it to ADDR_L[5:2] */
qspi_reg->spi_ctrlr0 = trans_type | (msg->address.size & ~0x03)
| (0x2 << 8) | (msg->dummy_cycles << 11);
qspi_reg->ctrlr0 &= ~((3 << 22) | (3 << 10));

/* Config SPI frame format and transmission mode */
if (length)
{
qspi_reg->ctrlr0 |= (tmod << 10);
qspi_reg->txftlr = (txfthr << 16) | (SSIC_TX_ABW / 2);
qspi_reg->rxftlr = (SSIC_RX_ABW - 1);
qspi_reg->imr = (1 << 11) | (1 << 8);
qspi_reg->dmacr = (1 << 6) | (3 << 3) | (1 << 2);
qspi_reg->ctrlr1 = length - 1;
qspi_reg->spidr = msg->instruction.content;
qspi_reg->spiar = msg->address.content;
if (tmod == SPI_TMOD_TO){
rt_memcpy(buf, msg_parent->send_buf, length * cell_size);
rt_hw_cpu_dcache_clean(buf, CACHE_ALIGN_TOP(length * cell_size));
}
qspi_reg->axiar0 = (rt_uint32_t)((uint64_t)buf);
qspi_reg->axiar1 = (rt_uint32_t)((uint64_t)buf >> 32);
}

对于只发送指令和地址而没有数据阶段的传输,驱动禁用DMA和中断,直接通过写数据寄存器发送指令和地址,然后轮询状态寄存器等待传输完成。这种方式避免了为短传输分配DMA缓冲区和处理中断的开销:

1
2
3
4
5
6
7
8
else{
tmod = SPI_TMOD_TO;
qspi_reg->ctrlr0 |= (tmod << 10);
qspi_reg->txftlr = ((SSIC_TX_ABW - 1) << 16) | (SSIC_TX_ABW - 1);
qspi_reg->rxftlr = (SSIC_RX_ABW - 1);
qspi_reg->imr = 0;
qspi_reg->dmacr = 0;
}

传输执行时先复位事件,然后使能片选和控制器。有数据传输时等待中断事件;无数据传输时直接写FIFO并轮询等待。传输完成后释放片选并禁用控制器,检查超时和DMA错误,最后处理接收数据并释放缓冲区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
rt_event_control(&qspi_bus->event, RT_IPC_CMD_RESET, 0);

qspi_reg->ser = 1;
qspi_reg->ssienr = 1;
rt_uint32_t event;
rt_err_t err;

/*
* Config QSPI address and instruction,
* if data is empty, unable dma and send address and instruction by write data register.
*/
if (length){
err = rt_event_recv(&qspi_bus->event, BIT(SSI_DONE) | BIT(SSI_AXIE),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, 1000, &event);
}else
{
err = RT_EOK;
event = 0;
qspi_reg->dr[0] = msg->instruction.content;
length++;
if (msg->address.size){
qspi_reg->dr[0] = msg->address.content;
length++;
}
qspi_reg->txftlr = 0;
/* Wait for SPI transfer to complete (busy-wait) */
while ((qspi_reg->sr & 0x5) != 0x4)
{
/* Busy wait */
}
}
qspi_reg->ser = 0;
qspi_reg->ssienr = 0;
if (err == -RT_ETIMEOUT){
LOG_E("qspi%d transfer data timeout", qspi_bus->idx);
if (buf){
rt_free_align(buf);
}
return 0;
}
if (event & BIT(SSI_AXIE)){
LOG_E("qspi%d dma error", qspi_bus->idx);
if (buf){
rt_free_align(buf);
}
return 0;
}
/* Read data from FIFO */
if (tmod == SPI_TMOD_RO){
rt_hw_cpu_dcache_invalidate(buf, CACHE_ALIGN_TOP(length * cell_size));
rt_memcpy(msg_parent->recv_buf, buf, length * cell_size);
}
if (buf){
rt_free_align(buf);
}
return length;

标准SPI传输

标准SPI传输使用中断方式而非DMA。驱动首先根据缓冲区是否空确定传输模式:只有发送缓冲区时为只发送模式(SPI_TMOD_TO),只有接收缓冲区时为只读模式(SPI_TMOD_RO),两者都有时为全双工模式(SPI_TMOD_TR)。对于只读模式,如果配置了地址,则切换为EEPROM读模式(SPI_TMOD_EPROMREAD):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (msg_parent->length == 0){
return 0;
}
rt_uint8_t cell_size = (qspi_cfg_parent->data_width + 7) >> 3;
rt_size_t length = msg_parent->length;
rt_size_t count = length > 0x10000 ? 0x10000 : length;
rt_size_t send_single = 0, send_length = 0, recv_single = 0, recv_length = 0;
void *send_buf = (void *)msg_parent->send_buf;
void *recv_buf = msg_parent->recv_buf;
rt_uint8_t tmod = send_buf ? SPI_TMOD_TO : SPI_TMOD_EPROMREAD;
tmod = recv_buf ? tmod & SPI_TMOD_RO : tmod;
// ...
if (tmod == SPI_TMOD_RO && qspi_cfg_parent->data_width == 8){
if ((msg->address.size & 7) || (msg->dummy_cycles & 7)){
// error message
return 0;
}
else if (msg->address.size){
if (length > 0x10000){
// error message
return 0;
}
tmod = SPI_TMOD_EPROMREAD;
}
}

缓冲区准备时,驱动为发送和接收分别分配临时缓冲区。对于EEPROM读模式,需要构造包含指令、地址和dummy字节的发送序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (send_buf){
send_single = count;
send_buf = rt_malloc(count * cell_size);
if (send_buf == RT_NULL){
LOG_E("alloc mem error");
return 0;
}
rt_memcpy(send_buf, msg_parent->send_buf, count * cell_size);
}
else if (tmod == SPI_TMOD_EPROMREAD){
send_single = 1 + msg->address.size / 8 + msg->dummy_cycles / 8;
send_buf = rt_malloc(send_single);
if (send_buf == RT_NULL)
{
LOG_E("alloc mem error");
return 0;
}
rt_uint8_t *temp = send_buf;
*temp++ = msg->instruction.content;
for (int i = msg->address.size / 8; i; i--){
*temp++ = msg->address.content >> ((i - 1) * 8);
}
for (int i = msg->dummy_cycles / 8; i; i--){
*temp++ = 0xFF;
}
}

在缓冲区准备完成后,驱动需要配置寄存器并启动传输。首先将缓冲区信息保存到设备结构体中,供中断处理函数使用:

1
2
3
4
5
qspi_bus->cell_size = cell_size;
qspi_bus->send_buf = send_buf;
qspi_bus->recv_buf = recv_buf;
qspi_bus->send_length = send_single;
qspi_bus->recv_length = recv_single;

接下来配置传输相关寄存器。ctrlr0设置传输模式,ctrlr1设置本次传输的数据长度。FIFO阈值配置和增强型SPI类似:发送阈值设为FIFO深度的一半,接收阈值根据传输长度动态调整。禁用DMA,使能发送空和接收满中断,配置完成后,复位事件清除之前的事件状态,然后拉低片选并使能控制器启动传输。对于只读模式,需要向数据寄存器写入一个dummy值来触发读操作

1
2
3
4
5
6
7
8
9
10
11
12
13
qspi_reg->ctrlr0 &= ~((3 << 22) | (3 << 10));
qspi_reg->ctrlr0 |= (tmod << 10);
qspi_reg->ctrlr1 = count - 1;
qspi_reg->txftlr = ((SSIC_TX_ABW / 2) << 16) | (SSIC_TX_ABW / 2);
qspi_reg->rxftlr = count >= (SSIC_RX_ABW / 2) ? (SSIC_RX_ABW / 2 - 1) : count - 1;
qspi_reg->dmacr = 0;
/* Interrupt transmit or receive */
qspi_reg->imr = (1 << 4) | (1 << 0);
rt_event_control(&qspi_bus->event, RT_IPC_CMD_RESET, 0);
qspi_reg->ser = 1;
qspi_reg->ssienr = 1;
if (tmod == SPI_TMOD_RO)
qspi_reg->dr[0] = 0;

传输线程在配置完寄存器并启动传输后,进入事件循环通过rt_event_recv阻塞等待中断事件。这个函数会挂起当前线程,直到接收到指定的事件或超时。

1
2
3
4
5
6
7
8
9
while (RT_TRUE){
rt_uint32_t event;
rt_err_t err = rt_event_recv(&qspi_bus->event,
BIT(SSI_TXE) | BIT(SSI_RXF),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
10000, &event);
// ...收发逻辑
}

当收到SSI_TXE事件时,表示当前批次的数据已经全部写入FIFO。如果还有剩余数据需要发送,驱动将下一批数据(最多64KB)复制到临时缓冲区,更新设备结构体中的缓冲区指针和长度,然后重新使能SSI_TXE中断。对于只发送模式,当所有数据都已发送后,需要轮询状态寄存器等待SPI控制器完成物理传输,因为最后一批数据可能还在FIFO中等待发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Handle Transmit buffer empty */
if (event & BIT(SSI_TXE)){
send_length += send_single;
if (send_length < length && tmod <= SPI_TMOD_TO){
count = length - send_length;
count = count > 0x10000 ? 0x10000 : count;
rt_memcpy(send_buf, msg_parent->send_buf + send_length * cell_size, count * cell_size);
qspi_bus->send_buf = send_buf;
qspi_bus->send_length = count;
send_single = count;
qspi_reg->txftlr = ((SSIC_TX_ABW / 2) << 16) | (SSIC_TX_ABW / 2);
if (tmod == SPI_TMOD_TO)
qspi_reg->imr |= (1 << 0);
}
else if (tmod == SPI_TMOD_TO)
{
/* Wait for SPI transfer to complete (busy-wait) */
while ((qspi_reg->sr & 0x5) != 0x4)
{
/* Busy wait */
}
break;
}
}

当收到SSI_RXF事件时,表示临时缓冲区已经接收满。驱动将数据复制到用户缓冲区,更新已接收的数据量。如果还有剩余数据需要接收,准备接收下一批数据。对于只读模式,每次只能配置一次接收长度(通过ctrlr1寄存器),因此需要禁用控制器、更新ctrlr1、重新使能控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* Handle receive buffer full */
if (event & BIT(SSI_RXF)){
rt_memcpy(msg_parent->recv_buf + recv_length * cell_size, recv_buf, recv_single * cell_size);
recv_length += recv_single;
if (recv_length >= length)
{
break;
}
count = length - recv_length;
count = count > 0x10000 ? 0x10000 : count;
qspi_bus->recv_buf = recv_buf;
qspi_bus->recv_length = count;
recv_single = count;
qspi_reg->rxftlr = count >= (SSIC_RX_ABW / 2) ? (SSIC_RX_ABW / 2 - 1) : count - 1;
if (tmod == SPI_TMOD_TR){
qspi_reg->imr |= (1 << 0) | (1 << 4);
}
else if (tmod == SPI_TMOD_RO){
qspi_reg->imr |= (1 << 4);
qspi_reg->ssienr = 0;
qspi_reg->ctrlr1 = count - 1;
qspi_reg->ssienr = 1;
/* Trigger two dummy transfers to restart SPI read in read-only mode.
* This is required by the hardware to ensure correct data reception.
*/
qspi_reg->dr[0] = 0;
qspi_reg->dr[0] = 0;
}
}

中断回调函数

中断回调函数是标准SPI传输的核心,负责实际的FIFO读写操作。函数通过中断向量号识别中断类型,然后执行相应的处理逻辑。

K230的每个SPI控制器有4个中断源,它们的中断向量号是连续的。通过计算向量号相对于IRQN_SPI0基址的偏移,可以确定是哪个控制器的哪种中断:

1
2
3
4
5
6
7
8
9
10
11
static void k230_spi_irq(int vector, void *param)
{
struct k230_spi_dev *qspi_bus = param;
k230_spi_reg_t *qspi_reg = (k230_spi_reg_t *)qspi_bus->base;


vector -= IRQN_SPI0;
vector %= (IRQN_SPI1 - IRQN_SPI0);
// ...
}

当发送FIFO中的数据量低于阈值时触发SSI_TXE中断。中断处理函数检查状态寄存器sr的bit1(Tx FIFO FULL),循环将数据写入FIFO直到缓冲区为空或FIFO满。根据cell_size(数据帧大小)选择8位、16位或32位写入方式。

当缓冲区数据全部写入FIFO后,处理逻辑根据传输模式有所不同。对于只发送模式(SPI_TMOD_TO),数据全部写入FIFO并不意味着传输完成,因为FIFO中的数据可能还在等待发送。驱动将发送阈值设为0,这样当FIFO完全清空时会再次触发TXE中断,此时才发送事件通知传输线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Handle transmit buffer empty interrupt */
if (vector == SSI_TXE){
if (qspi_bus->send_buf == RT_NULL){
qspi_reg->imr &= ~1;
}else if (qspi_bus->cell_size == 1){
while ((qspi_bus->send_length) && (qspi_reg->sr & 2)){
qspi_reg->dr[0] = *((rt_uint8_t *)qspi_bus->send_buf);
qspi_bus->send_buf++;
qspi_bus->send_length--;
}
}
// cell_size = 2/4
if (qspi_bus->send_length == 0){
if (((qspi_reg->ctrlr0 >> 10) & SPI_TMOD_EPROMREAD) == SPI_TMOD_TO){
if (qspi_reg->txftlr)
return;
}
qspi_reg->txftlr = 0;
qspi_reg->imr &= ~1;
rt_event_send(&qspi_bus->event, BIT(SSI_TXE));
}
}

当接收FIFO中的数据量达到阈值时触发SSI_RXF中断。中断处理函数检查状态寄存器sr的bit3(Receive FIFO EMPTY),循环从FIFO读取数据直到缓冲区满或FIFO空。同样根据cell_size选择读取方式。

当剩余数据量小于当前FIFO阈值时,动态调整阈值避免最后一批数据无法触发中断。例如,如果阈值设为31(FIFO深度的一半),但只剩20个数据要接收,FIFO永远不会达到31的阈值,导致接收卡死。将阈值调整为19(剩余数据-1)可以确保最后一批数据正确接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if (vector == SSI_RXF){
if (qspi_bus->recv_buf == RT_NULL){
qspi_reg->imr &= ~0x10;
}else if (qspi_bus->cell_size == 1){
while ((qspi_bus->recv_length) && (qspi_reg->sr & 8)){
*((rt_uint8_t *)qspi_bus->recv_buf) = qspi_reg->dr[0];
qspi_bus->recv_buf++;
qspi_bus->recv_length--;
}
}
// cell_size = 2/4
if (qspi_bus->recv_length == 0){
qspi_reg->imr &= ~0x10;
rt_event_send(&qspi_bus->event, BIT(SSI_RXF));
}
else if (qspi_bus->recv_length <= qspi_reg->rxftlr){
qspi_reg->rxftlr = qspi_bus->recv_length - 1;
}
}

传输完成中断(DONE)和AXI错误中断(AXIE)主要用于增强型SPI的DMA传输模式。中断处理函数只需读取相应的清除寄存器来清除中断标志,然后发送事件通知传输线程:

1
2
3
4
5
6
7
8
else if (vector == SSI_DONE){
(void)qspi_reg->donecr; // 读取清除中断
rt_event_send(&qspi_bus->event, BIT(SSI_DONE));
}
else if (vector == SSI_AXIE){
(void)qspi_reg->axiecr; // 读取清除中断
rt_event_send(&qspi_bus->event, BIT(SSI_AXIE));
}

中断触发时会屏蔽调度器,导致操作系统无法进行正常的任务调度。因此驱动设计时,中断处理函数应该尽量轻量,只做必要的FIFO读写等操作,通过事件通知机制触发传输线程处理耗时操作。


K230-SPI驱动移植
https://chuann-sudo.github.io/2025/12/18/K230-SPI驱动移植/
作者
ChuanN
发布于
2025年12月18日
许可协议