SPI模块介绍
参考<<Renesas RA2L1 Group User’s Manual: Hardware>>的章节<<28. Serial Peripheral Interface (SPI) >>
特征
- 2通道
- 支持全双工和仅发送模式,支持3线制和4线制
- RSPCK极性反转,相位翻转
- MSB和LSB可设置
- 位长可设置为 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 32。
- 32位收发缓存,支持双缓存。
- 主模式下,RSPCK可以由PCLKB分频2~4096得到,从模式下RSPCK最大频率为PCLKB4分频。
- 支持模式错误,校验错误,上下溢出错误检测
- 支持四个片选SSL,单主模式 SSLn0~ SSLn3作为输出;多主模式 SSLn0输入,SSLn1~ SSLn3作为输出或者不用;从模式 SSLn0输入,SSLn1~ SSLn3不用。
- SSL有效到RSPCK产生时钟输出可以配置 1 ~ 8 个RSPCK延迟;RSPCK停止产生时钟输出到SSL无效可以配置 1 ~ 8 个RSPCK延迟。
- SSL无效到下次开始操作有效可以配置 1 ~ 8 个RSPCK延迟。
- 可以直接控制SSL极性
- burst可配置
- 支持以下中断源,接收缓冲满,发送缓冲空,检测到错误,SPI空闲,发送完成。
- 以上中断源支持连接到ELC模块
- 支持配置回环模式
- 支持输出引脚CMOS和开漏配置
- 可以STOP模块支持降低功耗
模块框图如下
引脚
寄存器
这里列举下各寄存器的作用,大致了解下功能,实际编程时可以参考手册。
SPCR:控制寄存器,模块模式,中断使能,模块使能等配置。
SSLP:从模式SSL极性配置
SPPCR:引脚回环设置,MOSI空闲时电平设置
SPSR:状态寄存器,可以看手法缓冲区状态,空闲状态,错误状态
SPDR/SPDR_HA/SPDR_BY:数据寄存器,根据 SPDCR.SPBYT, SPDCR.SPBYT的设置可以按照32位,16位,8位方式访问。收发都是访问该寄存器。
对于发送直接写寄存器就是写发送缓存
对于接收要设置SPDCR.SPRDTD用于选择读收还是法缓存
SPBR:波特率计算寄存器
计算公式为
其中PCLK为FPCLKB
n为该寄存器的值0~255
N为SPCMD0.BRDV[1:0]即分频值。
SPDCR:数据控制寄存器,设置 SPDR访问的方式,和读 SPDR时是读受还是发缓存。
SPCKD:SPCMD0.SCKDEN=1时设置 SSLni有效到RSPCK产生时钟延迟RSPCK时钟的个数,从模式要设置为0。
SSLND:SPCMDn .SLNDEN=1时设置RSPCK时钟停止到SSLni无效延迟RSPCK时钟的个数,从模式要设置为0。
SPND:SPCMD0.SPNDEN=1时设置SSLni无效到下一次有效延迟RSPCK时钟的个数,从模式要设置为0。
SPCR2:控制寄存器2,校验等设置
SPCMD0:SPI的模式,数据位数等设置。
操作
操作模式设置
支持一下模式
引脚设置
PmnPFS.NCODR
MOSI在空闲时可以配置为以下状态
其他数据格式,SPI模式,错误检测中断等参考手册。
软件关键操作
以下是一些关键操作可以参考手册
PCLKB时钟
MSTPCRB使能SPI模块才能访问SPI寄存器,默认是不使能的。
设置中断
设置引脚
SPCR.SPE=0初始化
SPI各寄存器配置 如果使用DMADTC配置
使能SPI
中断处理
添加SPI驱动
安装RA Configuration
https://www2.renesas.cn/kr/en/software-tool/ra-smart-configurator#overview下下载,
或者wget https://github.com/renesas/fsp/releases/download/v4.0.0/setup_fsp_v4_0_0_rasc_v2022-07.exe
下载慢可以通过以下地址加速下载
https://gh.api.99988866.xyz/
下载后点击安装
添加SPI代码
SCI0和SPI0引脚重复,不使用SCI0,使用SPI0
添加如下SCI0的中断
名字分别为
spi_eri_isr,spi_rxi_isr,spi_tei_isr,sppi_txi_isr不添加idle中断
设置属性,如下
添加的代码如下
驱动代码位于
rt-thread\bsp\renesas\libraries\HAL_Drivers\drv_spi.c和drv_spi.h
添加到工程Drivers目录下。
rtconfig.h中添加
#define RT_USING_SPI
bsp_mcu_family_cfg.h注释掉#include "bsp_mcu_device_pn_cfg.h"
bsp_cfg.h中注释掉#include "board_cfg.h"
r_spi.c中实现了中断服务函数
void spi_rxi_isr(void);
void spi_txi_isr(void);
void spi_tei_isr(void);
void spi_eri_isr(void);
与vector_data.c和vector_data.h中对应。如果编译有错误可以看下这里名字是否对应。
drv_spi.c中spi0_callback改为spi_callback
编译还提示rt_spi_bus_attach_device找不到。
将\rt-thread\components\drivers\spi下的
spi_core.c
spi_dev.c
drv_spi.c添加到
工程Drivers下
配置
rtconfig.h中
添加宏
#define RT_USING_SPI
#define BSP_USING_SPI0
#define RT_USING_DEVICE_OPS 必须要否则
rt_spi_bus_device_init中
#ifdef RT_USING_DEVICE_OPS
device->ops = &spi_bus_ops;
#else
不会执行
rt_spi_configure时会调用device->bus->ops->configure(device, &device->config);空函数指针导致hardfault。
编译OK
运行输入list device就可以看到由spi0的设备了。
msh >list device
device type ref count
-------- -------------------- ----------
spi0 SPI Bus 0
uart9 Character Device 2
pin Pin Device 0
msh >
SPI驱动的使用
参考https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/spi/spi
//需要包含spi.h
//drv_spi.h
挂载SPI设备
drv_spi.c中rt_hw_spi_device_attach调用rt_spi_bus_attach_device
配置SPI设备
rt_spi_configure
其他接口参考网页文档
函数
|
描述
|
rt_device_find()
|
根据 SPI 设备名称查找设备获取设备句柄
|
rt_spi_transfer_message()
|
自定义传输数据
|
rt_spi_transfer()
|
传输一次数据
|
rt_spi_send()
|
发送一次数据
|
rt_spi_recv()
|
接受一次数据
|
rt_spi_send_then_send()
|
连续两次发送
|
rt_spi_send_then_recv()
|
先发送后接收
|
SPI驱动分析
初始化过程
定义了RT_USING_SPI时
drv_spi.c中
INIT_BOARD_EXPORT(ra_hw_spi_init);
展开就是
__attribute__((used)) const init_fn_t __rt_init_ra_hw_spi_init __attribute__((section(x)))(.rti_fn.1) = fn
也就是定义了typedef int (*init_fn_t)(void);函数指针类型的变量
__rt_init_ra_hw_spi_init其值就是函数ra_hw_spi_init,变量放在了.rti_fn.1段。
类似的以下分别定义了连个变量
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
const init_fn_t __rt_init_rti_board_start __attribute__((section(.rti_fn.0.end))) = rti_board_start
const init_fn_t __rt_init_rti_board_end __attribute__((section(.rti_fn.1.end))) = rti_board_end
上面定义的__rt_init_ra_hw_spi_init位于段.rti_fn.1
所以位于段.rti_fn.0.end和段.rti_fn.1.end之间(按照字母顺序排列)。
rt_components_board_init函数中
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
就是遍历上述定义的两个变量之间,从__rt_init_rti_board_start开始找到__rt_init_rti_board_end结束
中间就会找到__rt_init_ra_hw_spi_init调用其值指向的函数即调用ra_hw_spi_init
调用过程如下
rtthread_startup->
rt_hw_board_init->
rt_components_board_init->
ra_hw_spi_init->
rt_spi_bus_register->
互斥量
其中rt_spi_bus_register时会创建互斥量
rt_mutex_init(&(bus->lock), name, RT_IPC_FLAG_PRIO);
rt_spi_send_then_recv等接口中
rt_mutex_take会使用。
cs引脚
ra_hw_spi_configure时spi_dev->cs_pin = (rt_uint32_t)device->parent.user_data;会用到
所以rt_spi_bus_attach_device(&spi_dev,"spi00", "spi0", (void*)spi_cs);注意这里spi_cs前不要加&不是传其地址,而是就是传其值,只不过强制转换为(void*).
(rt_uint32_t)device->parent.user_data用的时候还是会强制转为(rt_uint32_t)。
顺便提一下pin驱动
drv_pgio.c中
const static struct rt_pin_ops _ra_pin_ops实现了io操作接口。
比如ra_pin_write
最终是调用R_BSP_PinWrite
通过bsp_io_port_pin_t可以看到
我们spi0使用的是p103所以对应的pin号是
BSP_IO_PORT_10_PIN_03 = 0x0A03, ///< IO port 10 pin 3
spi底层配置
rt_spi_configure->
device->bus->ops->configure(device, &device->config);->
ra_hw_spi_configure->
R_SPI_Open
进行相应的底层初始化
其中ra_hw_spi_configure在
rt_err_t err = rt_spi_bus_register(&spi_config[spi_index].bus, spi_handle[spi_index].bus_name, &ra_spi_ops);时注册
static const struct rt_spi_ops ra_spi_ops =
{
.configure = ra_hw_spi_configure,
.xfer = ra_spixfer,
};
中注册的
此时可以看到spi0总线和spi00设备
msh >list device
device type ref count
-------- -------------------- ----------
spi00 SPI Device 0
spi0 SPI Bus 0
uart9 Character Device 2
pin Pin Device 0
msh >
回环测试
将P100 J2-16和P101 J2-15短接在一起,也就是SPI0的MISO和MOSI短接在一起。
测试代码如下
hal_entry.c
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2021-10-10 Sherman first version
*/
#include <rtthread.h>
#include "hal_data.h"
#include <rtdevice.h>
#include <drv_spi.h>
#define LED1_PIN "P502" /* Onboard LED pins */
#define USER_INPUT "P004"
void hal_entry(void)
{
rt_kprintf("\nHello RT-Thread!\n");
rt_uint32_t led1_pin = rt_pin_get(LED1_PIN);
uint8_t sendbuf[6]={0,1,2,3,4,5};
uint8_t readbuf[6]={0,0,0,0,0,0};
rt_err_t res;
static struct rt_spi_device spi_dev;
static rt_uint32_t spi_cs = BSP_IO_PORT_10_PIN_03;
res = rt_spi_bus_attach_device(&spi_dev,"spi00", "spi0", (void*)spi_cs);
if (res != RT_EOK)
{
rt_kprintf("rt_spi_bus_attach_device!\r\n");
}
else
{
rt_pin_mode(spi_cs, PIN_MODE_OUTPUT);
rt_pin_write(spi_cs, PIN_HIGH);
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.max_hz = 12000000;
cfg.mode = RT_SPI_MODE_1 | RT_SPI_MSB;
spi_dev.bus->owner = &spi_dev;
rt_spi_configure(&spi_dev,&cfg);
rt_kprintf("spi0 send:\r\n");
for(int i=0;i<sizeof(sendbuf);i++)
{
rt_kprintf("%02x ",sendbuf);
}
rt_kprintf("\r\n");
rt_spi_transfer(&spi_dev, sendbuf, readbuf, sizeof(sendbuf));
rt_kprintf("spi0 rcv:\r\n");
for(int i=0;i<sizeof(readbuf);i++)
{
rt_kprintf("%02x ",readbuf);
}
rt_kprintf("\r\n");
}
while (1)
{
rt_pin_write(led1_pin, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(led1_pin, PIN_LOW);
rt_thread_mdelay(500);
}
}
void irq_callback_test(void *args)
{
rt_kprintf("\n IRQ03 triggered \n");
}
void icu_sample(void)
{
/* init */
rt_uint32_t pin = rt_pin_get(USER_INPUT);
rt_kprintf("\n pin number : 0x%04X \n", pin);
rt_err_t err = rt_pin_attach_irq(pin, PIN_IRQ_MODE_RISING, irq_callback_test, RT_NULL);
if (RT_EOK != err)
{
rt_kprintf("\n attach irq failed. \n");
}
err = rt_pin_irq_enable(pin, PIN_IRQ_ENABLE);
if (RT_EOK != err)
{
rt_kprintf("\n enable irq failed. \n");
}
}
MSH_CMD_EXPORT(icu_sample, icu sample);
看到收到的和发送的一致说明回环测试OK,该测试不能测试CLK和CS,CLK和CS可以使用逻辑分析监控。
总结
rt-thread的spi设备驱动比较绕,对于新手来说会遇到很多坑,任何一个坑都可能导致异常,这是一个非常痛苦的过程,个人认为搞得太复杂了。
官方的文档
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/spi/spi?id=spi-%e8%ae%be%e5%a4%87%e4%bd%bf%e7%94%a8%e7%a4%ba%e4%be%8b
只是单纯的描述各个API接口,并没有解释说明,spi驱动的架构,怎么初始化,设备怎么注册,底层到底是怎么配置的,cs引脚到底是怎么关联的等等,文档有待加强。对于一个初使用该系统环境的开发人员来说十分不友好。
另外驱动太绕,各种指针回调,反复绕,实际上是可以简化的,没必要搞得这么复杂,对于嵌入式简单即是美。
另外一方面驱动并没有完全抽象出来,理论上驱动应该是完全可移植的,底层只依赖一组最小的HAL操作接口,比如SPI的send,read,收发中断回调,参数配置等这几个接口,驱动层只应该调用这几个接口,这几个接口不同芯片不同实现即可。而不是整个drv_xxx.c都要根据不同芯片改写。drv_xxx.c应该是完全复用的,其实现缓冲取,消息对列等操作应该是完全抽象可移植的。而现在的做法是每个芯片都实现一套bsp,每套bsp里都要写drv文件,工作量太大了,没有分好层次,严格界定驱动和HAL的界限,反而是搞了一套设备的注册接口,各种回调,指针,搞得很复杂,有点注重形式轻内在了。现在这种做法是方便编译构建,但是没有真正的分好驱动层的架构,使用容易出错且复杂不好调试。