SPI 概述

Serial Peripheral interface 通用串行外围设备接口是 Motorola 首先在其 MC68HCXX 系列处理器上定义的。SPI 接口主要应用在 EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。

 

SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为 PCB 的布局上节省空间。

 

SPI 特点

采用主 - 从模式(Master-Slave) 的控制方式

SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave). 一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作。

 

而这里的 SPI 中的时钟和相位,指的就是 SCLk 时钟的特性,即保证主从设备两者的时钟的特性一致了,以保证两者可以正常实现 SPI 通讯。

 

采用同步方式(Synchronous)传输数据

Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的 。

 

 

工作机制

概述

首先看下 SPI Data Transfer 模块图。

 

 

上图只是对 SPI 设备间通信的一个简单的描述, 下面详细解释一下图中所示的几个组件(Module):

 

SSPBUF

Synchronous Serial Port Buffer, 泛指 SPI 设备里面的内部缓冲区, 一般在物理上是以 FIFO 的形式, 保存传输过程中的临时数据;

 

 

我们知道, 在每个时钟周期内, Master 与 Slave 之间交换的数据其实都是 SPI 内部移位寄存器从 SSPBUF 里面拷贝的 . 我们可以通过往 SSPBUF 对应的寄存器 (Tx-Data / Rx-Data register) 里读写数据, 间接地操控 SPI 设备内部的 SSPBUF。

 

例如, 在发送数据之前, 我们应该先往 Master 的 Tx-Data 寄存器写入将要发送出去的数据, 这些数据会被 Master-SSPSR 移位寄存器根据 Bus-Width 自动移入 Master-SSPBUF 里, 然后这些数据又会被 Master-SSPSR 根据 Channel-Width 从 Master-SSPBUF 中移出, 通过 Master-SDO 管脚传给 Slave-SDI 管脚, Slave-SSPSR 则把从 Slave-SDI 接收到的数据移入 Slave-SSPBUF 里 . 与此同时, Slave-SSPBUF 里面的数据根据每次接收数据的大小(Channel-Width), 通过 Slave-SDO 发往 Master-SDI, Master-SSPSR 再把从 Master-SDI 接收的数据移入 Master-SSPBUF. 在单次数据传输完成之后, 用户程序可以通过从 Master 设备的 Rx-Data 寄存器读取 Master 设备数据交换得到的数据。

 

SSPSR

Synchronous Serial Port Register, 泛指 SPI 设备里面的移位寄存器(Shift Regitser), 它的作用是根据设置好的数据位宽(bit-width) 把数据移入或者移出 SSPBUF;

 

 

SSPSR 是 SPI 设备内部的移位寄存器(Shift Register). 它的主要作用是根据 SPI 时钟信号状态, 往 SSPBUF 里移入或者移出数据, 每次移动的数据大小由 Bus-Width 以及 Channel-Width 所决定。Bus-Width 的作用是指定地址总线到 Master 设备之间数据传输的单位 . 例如, 我们想要往 Master 设备里面的 SSPBUF 写入 16 Byte 大小的数据: 首先, 给 Master 设备的配置寄存器设置 Bus-Width 为 Byte; 然后往 Master 设备的 Tx-Data 移位寄存器在地址总线的入口写入数据, 每次写入 1 Byte 大小的数据(使用 writeb 函数); 写完 1 Byte 数据之后, Master 设备里面的 Tx-Data 移位寄存器会自动把从地址总线传来的 1 Byte 数据移入 SSPBUF 里; 上述动作一共需要重复执行 16 次。

 

Channel-Width 的作用是指定 Master 设备与 Slave 设备之间数据传输的单位 . 与 Bus-Width 相似, Master 设备内部的移位寄存器会依据 Channel-Width 自动地把数据从 Master-SSPBUF 里通过 Master-SDO 管脚搬运到 Slave 设备里的 Slave-SDI 引脚, Slave-SSPSR 再把每次接收的数据移入 Slave-SSPBUF 里 . 通常情况下, Bus-Width 总是会大于或等于 Channel-Width, 这样能保证不会出现因 Master 与 Slave 之间数据交换的频率比地址总线与 Master 之间的数据交换频率要快, 导致 SSPBUF 里面存放的数据为无效数据这样的情况

 

Controller

泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式。通常情况下, 我们只需要对上图所描述的四个管脚(pin) 进行编程即可控制整个 SPI 设备之间的数据通信。

 

 

Master 设备里面的 Controller 主要通过时钟信号(Clock Signal)以及片选信号(Slave Select Signal)来控制 Slave 设备 . Slave 设备会一直等待, 直到接收到 Master 设备发过来的片选信号, 然后根据时钟信号来工作。

 

Master 设备的片选操作必须由程序所实现 . 例如: 由程序把 SS/CS 管脚的时钟信号拉低电平, 完成 SPI 设备数据通信的前期工作; 当程序想让 SPI 设备结束数据通信时, 再把 SS/CS 管脚上的时钟信号拉高电平 .

 

SCK

Serial Clock, 主要的作用是 Master 设备往 Slave 设备传输时钟信号, 控制数据交换的时机以及速率;

 

SS/CS

Slave Select/Chip Select, 用于 Master 设备片选 Slave 设备, 使被选中的 Slave 设备能够被 Master 设备所访问。

 

SDO/MOSI

Serial Data Output/Master Out Slave In, 在 Master 上面也被称为 Tx-Channel, 作为数据的出口, 主要用于 SPI 设备发送数据。

 

SDI/MISO

Serial Data Input/Master In Slave Out, 在 Master 上面也被称为 Rx-Channel, 作为数据的入口, 主要用于 SPI 设备接收数据。

 

SPI 设备在进行通信的过程中, Master 设备和 Slave 设备之间会产生一个数据链路回环(Data Loop), 就像上图所画的那样, 通过 SDO 和 SDI 管脚, SSPSR 控制数据移入移出 SSPBUF, Controller 确定 SPI 总线的通信模式, SCK 传输时钟信号。

 

极性和相位

要想搞清楚 SPI 的数据传输,首先要搞清楚相位和极性的概念,即 SPI 的极性 Polarity 和相位 Phase。

 

最常见的写法是 CPOL 和 CPHA,不过也有一些其他写法,简单总结如下:

  • (1) CKPOL (Clock Polarity) = CPOL = POL = Polarity = (时钟)极性(2) CKPHA (Clock Phase)   = CPHA = PHA = Phase = (时钟)相位(3) SCK=SCLK=SPI 的时钟(4) Edge=边沿,即时钟电平变化的时刻,即上升沿(rising edge)或者下降沿(falling edge)

 

对于一个时钟周期内,有两个 edge,分别称为:

(1)Leading edge=前一个边沿=第一个边沿,对于开始电压是 1, 那么就是 1 变成 0 的时候,对于开始电压是 0,那么就是 0 变成 1 的时候;

(2)Trailing edge=后一个边沿=第二个边沿,对于开始电压是 1, 那么就是 0 变成 1 的时候(即在第一次 1 变成 0 之后,才可能有后面的 0 变成 1), 对于开始电压是 0,那么就是 1 变成 0 的时候;

 

本博文采用如下用法:

极性=CPOL 相位=CPHASCLK=时钟第一个边沿和第二个边沿

 

CPOL 和 CPHA,分别都可以是 0 或时 1,对应的四种组合就是:

模式 极性 相位
Mode 1 CPOL=0,CPHA=0
Mode 2 CPOL=0,CPHA=1
Mode 3 CPOL=1,CPHA=0
Mode 4 CPOL=1,CPHA=1

 

采样示意图

 

CPOL=0,CPHA=0

脉冲传输前和完成后都保持在低电平状态,所以 CPOL=0,即低电平是空闲时的电平。在第一个边沿(上升沿)采样数据,第二个边沿(下降沿)输出数据,对应着 CPHA=0。

CPOL=0,CPHA=1

脉冲传输前和完成后都保持在低电平状态,所以 CPOL=0,即低电平是空闲时的电平。在第二个边沿(下降沿)采样数据,第一个边沿(上升沿)输出数据,对应着 CPHA=1。

CPOL=1,CPHA=0

脉冲传输前和完成后都保持在高电平状态,所以 CPOL=1,即高电平是空闲时的电平。在第一个边沿(下降沿)采样数据,第二个边沿(上升沿)输出数据,对应着 CPHA=0。

CPOL=1,CPHA=1

脉冲传输前和完成后都保持在高电平状态,所以 CPOL=1,即高电平是空闲时的电平。在第二个边沿(上升沿)采样数据,第一个边沿(下降沿)输出数据,对应着 CPHA=1。

CPOL=1,CPHA=1

 

软件中如何设置 SPI 的极性和相位

SPI 分主设备和从设备,两者通过 SPI 协议通讯。而设置 SPI 的模式,是从设备的模式,决定了主设备的模式。所以要先去搞懂从设备的 SPI 是何种模式,然后再将主设备的 SPI 的模式,设置和从设备相同的模式,即可正常通讯。

 

对于从设备的 SPI 是什么模式,有两种:

(1)固定的,有 SPI 从设备硬件决定的 SPI 从设备,具体是什么模式,相关的 datasheet 中会有描述,需要自己去 datasheet 中找到相关的描述,即:关于 SPI 从设备,在空闲的时候,是高电平还是低电平,即决定了 CPOL 是 0 还是 1; 然后再找到关于设备是在上升沿还是下降沿去采样数据,这样就是,在定了 CPOL 的值的前提下,对应着可以推算出 CPHA 是 0 还是 1 了。

 

(2)可配置的,由软件自己设定 从设备也是一个 SPI 控制器,4 种模式都支持,此时只要自己设置为某种模式即可。然后知道了从设备的模式后,再去将 SPI 主设备的模式,设置为和从设备模式一样即可。对于如何配置 SPI 的 CPOL 和 CPHA 的话,不多细说,多数都是直接去写对应的 SPI 控制器中对应寄存器中的 CPOL 和 CPHA 那两位,写 0 或写 1 即可。

 

数据交换(Data Exchanges)

SPI 只有主模式和从模式之分,没有读和写的说法,因为实质上每次 SPI 是主从设备在交换数据。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。

 

SPI 设备间的数据传输之所以又被称为数据交换, 是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 "发送者(Transmitter)" 或者 "接收者(Receiver)"。在每个 Clock 周期内, SPI 设备都会发送并接收一个 bit 大小的数据, 相当于该设备有一个 bit 大小的数据被交换了。

 

一个 Slave 设备要想能够接收到 Master 发过来的控制信号, 必须在此之前能够被 Master 设备进行访问 (Access)。所以, Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上。 在数据传输的过程中, 每次接收到的数据必须在下一次数据传输之前被采样 . 如果之前接收到的数据没有被读取, 那么这些已经接收完成的数据将有可能会被丢弃, 导致 SPI 物理模块最终失效。

 

因此, 在程序中一般都会在 SPI 传输完数据后, 去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的。

 

SPI 举例

下面举一个例子帮助大家理解。

 

SPI 是一个环形总线结构,由 ss(cs)、sck、sdi、sdo 构成,其时序其实很简单,主要是在 sck 的控制下,两个双向移位寄存器进行数据交换。

 

假设下面的 8 位寄存器装的是待发送的数据 10101010,上升沿发送、下降沿接收、高位先发送。那么第一个上升沿来的时候 数据将会是 sdo=1;寄存器=0101010x。下降沿到来的时候,sdi 上的电平将所存到寄存器中去,那么这时寄存器=0101010sdi,这样在 8 个时钟脉冲以后,两个寄存器的内容互相交换一次。这样就完成里一个 spi 时序。

 

举例:假设主机和从机初始化就绪:并且主机的 sbuff=0xaa,从机的 sbuff=0x55,下面将分步对 spi 的 8 个时钟周期的数据情况演示一遍,假设上升沿发送数据

 

 

这样就完成了两个寄存器 8 位的交换,上面的上表示上升沿、下表示下降沿,sdi、sdo 相对于主机而言的。

 

下一步就是把 上面的过程转为动画

 

交换数据

交换前

在这里插入图片描述

 

 

请仔细比较下交换后的 bit 顺序。

 

了解了 SPI 协议说明之后,下面我们基于 Cortex-A9 架构的 exynos-4412,讲解 SPI 控制器的使用。

 

Cortex-A9 SPI 控制器

硬件设计

本例是基于 FS4412 开发板,SPI 控制器外接了 MCP2515,MCP2515 与 exynos-4412 的硬件连接图如下图所示。

 

 

管脚连接说明

由上图可知 SPI 个引脚与 SOC 的 pin 连接关系:

CS    <---------> BUF_BK_LED   <---------> GPC1_2

SO    <---------> BUF_I2C_SDA6     <---------> GPC1_3

SI      <---------> BUF_I2C_SCL6       <---------> GPC1_4

SCK  <---------> BUF_GPC1_1        <---------> GPC1_1

INT   <-------------------------------------------> BUF_GPX0_0

MCP2515 芯片连接在 4412 芯片的 SPI2 上。

 

中断连接在 GPX0_0 上;CS、SO、SI、SCK 复用了 GPIO 引脚 GPC1 的引脚。

 

MCP2515 输出连接 SN65HVD230 CAN 总线收发器,SN65HVD230 是德州仪器公司生产的 3.3V CAN 收发器。为了节省功耗,缩小电路体积,MCP2515 CAN 总线控制器的逻辑电平采用 LVTTL,SN65HVD230 就是与其配套的收发器。

 

Cortex-A9 SPI 控制器

exynos4412 scp 中的串行外设接口(SPI)通过各种外设来传输串行数据。SPI 包括两个 8、16 和 32 位移位寄存器,用于传输和接收数据。在 SPI 传输过程中,它同时传输(串行移出)和接收(串行移位)数据。

 

特性

  • 全双工用于 Tx/Rx 的 8/16/32 位移位寄存器支持 8 位 /16 位 /32 位总线接口支持摩托罗拉 SPI 协议和 National Semiconductor Microwire 两个独立的 32 位宽的发送和接收 FIFO:端口 0 的深度为 64,端口 1 和 2 中的深度为 16 主模式和从模式接收而不发送操作发送 / 接收最高频率为 50 MHz

 

SPI 的操作

SPI 在 Exynos 4412 SCP 和外部设备之间传输 1 位串行数据。Exynos 4412 SCP 中的 SPI 支持 CPU 或 DMA 分别同时发送或接收 FIFO 和双向传输数据。SPI 有两个信道,即 Tx 信道和 Rx 信道。Tx 信道有来自 Tx 的路径 FIFO 到外部设备。Rx 通道有从外部设备到 Rx FIFO 的路径。

 

CPU(或 DMA)必须将数据写入寄存器 SPI_TX_DATA,才能将数据写入 FIFO。寄存器上的数据会自动移动到 Tx FIFO。要从 Rx FIFO 读取数据,CPU(或 DMA)必须访问寄存器 SPI_RX_DATA,数据会自动发送到 SPI_RX_DATA 寄存器。

 

CMU 寄存器可以控制 SPI 的工作频率。

 

操作模式

SPI 有两种模式,即主模式和从模式。在主模式下,生成 SPICLK 并将其传输到外部设备。XspiCS#是选择从机的信号,它指示在设置 XspiCS 时数据有效低水平。在发送或接收数据包之前,必须将 XspiCS 设置为低。

 

FIFO 存取

SPI 支持对 fifo 的 CPU 访问和 DMA 访问。对 fifo 的 CPU 访问和 DMA 访问的数据大小从 8 位、16 位或 32 位数据中选择。当它选择 8 位数据大小时,有效位是 0 到 7 比特。用户可以定义触发阈值来引发 CPU 中断。端口 0 中每个 FIFO 的触发电平由从 0 到 252 字节的步进为 4 个字节,端口 1 中每个 FIFO 的字节从 0 到 63 字节按 1 个字节的步长设置。SPI_MODE_CFG 寄存器的 TxDMAOn 或 RxDMAOn 位必须设置为使用 DMA 访问。DMA 访问支持只有单次传输和 4 突发传输。在 Tx FIFO 中,DMA 请求信号是高的,直到 Tx FIFO 满为止。在 Rx FIFO 中,如果 FIFO 不为空,DMA 请求信号高。

 

片选控制

芯片选择 XspiCS 是激活的低信号。换句话说,当 XspiCS 输入为 0 时,选择芯片。您可以自动或手动控制 XspiCS。不需要改变。手动模式当您使用手动控制模式时,您应清除 AUTO_N_MANUAL(默认值为 0)。NSSOUT 位控制 XspiCS 级别。自动模式使用自动控制模式时,必须将 AUTO_N_MANUAL 设置为 1。XspiCS 在数据包和自动打包。NCS_TIME_COUNT 控制 XspiCS 的非激活期。NSSOUT 在此时不可用。

 

SPI 寄存器说明

配置寄存器 CH_CFGn

 

【bit:5】  软件复位的时候必须先将此位设置为 1,给一个延时后再设置为 0;【bit:4】  设置 SPI 端口是主机还是从机,0:主机,1:从机;【bit:2-3】设置相位和极性;【bit:1】接收数据的通道使能位;【bit:0】发送数据的通道使能位。

 

数据宽度寄存器 MODE_CFGn

设置总线数据宽度,一般设置为 1 个字节。

片选寄存器 CS_REGn

只有 NSSOUT 拉低,才会进行数据的传输

 

 

移位寄存器状态寄存器 SPI_STATUSn

 

发送操作开始,如果移位寄存器空了,该值置 1,通过该值判断数据是否发送出去。

 

发送缓冲寄存器 SPI_TX_DATA

SPI_TX_DATA

 

接收缓冲寄存器 SPI_RX_DATA

SPI_RX_DATA

 

SPI 初始化流程

  1. 设置 GPIO 引脚为 SPI 模式;设置 clock;软件复位;设置 CPOL CPHA  为 00 模式,并设置为主机模式;设置数据位;片选。

 

1. 设置 GPIO 引脚为 SPI 模式

因为 CS、SO、SI、SCK 复用了 GPIO 引脚 GPC1 的引脚。首先需要设置 GPIO 引脚为 SPI 模式。

 

如上所示,该寄存器设置方式如下:

GPC1.CON = (GPC1.CON & ~0xffff0) | 0x55550;// 设置 IO 引脚为 SPI 模式

 

Step 1  设置 CPOL CPHA  为 00 模式

 

 

SPI2.CH_CFG&=~((0x1<< 4)|(0x1<<3)|(0x1<< 2)|0x3);

//master mode, CPOL = 0, CPHA = 0 (Format A)

 

2 设置 clock

时钟的设置需要依赖锁相环(PLL)时钟产生器。

 

 

从 30.2.1 节可知,时钟配置需要参考 CMU 这一章。

 

由上图可知 SPI 的 clock 源是 SCLK_SPI。

搜索 SCLK_SPI

 

从第七章搜所有的 SPI

 

继续搜索

 

由此可知 SPI0~2 的之中受 CLKMPLL_USER_T 控制,继续搜索。而此时钟位于寄存器 CLK_SRC_PERIL1 的 bit[27:24]。

 

继续搜索 SPI2。

 

 

搜索到 CLK_SRC_MASK_PERIL1、CLK_DIV_PERIL2。可知寄存器 CLK_SRC_MASK_PERIL1 默认是打开的,所以不用设置。CLK_DIV_PERIL2 用来设置 SPI2 分频的,于是寄存器设置如下:

CLK_SRC_PERIL1 = (CLK_SRC_PERIL1 & ~(0xF<<24)) | 6<<24;
// 0x6: 0110 = SCLKMPLL_USER_T 800Mhz
CLK_DIV_PERIL2 = 19 <<8 | 3;//SPI_CLK = 800/(19+1)/(3+1)

 

3. 软件复位

在这里插入图片描述

void soft_reset(void)
{ SPI2.CH_CFG |= 0x1 << 5;
 delay(1);                     // 延时
 SPI2.CH_CFG &= ~(0x1 << 5);
}

4. 设置 CPOL CPHA  为 00 模式,并设置为主机模式

SPI2.CH_CFG &= ~( (0x1 << 4) | (0x1 << 3) | (0x1 << 2) | 0x3);

5.  设置数据位 MODE_CFG

MODE_CFG

SPI2.MODE_CFG &= ~((0x3 << 17) | (0x3 << 29));
   //BUS_WIDTH=8bit,CH_WIDTH=8bit 

 

6.  片选

SPI2.CS_REG &= ~(0x1 << 1);        // 选择手动选择芯片

初始化搞定后,下面我们来看如何利用 SPI 收发数据。

收发数据流程

收发数据流程如下:

  1. spi 复位片选从机收发数据取消片选

【注意】一下代码为设置每次只读写 1 个 byte 数据,每次读写 1 个 byte 数据都要按照上述步骤操作。

1 spi 复位

void soft_reset(void)
{ SPI2.CH_CFG |= 0x1 << 5;
  delay(1);                     // 延时
  SPI2.CH_CFG &= ~(0x1 << 5);
}

2 片选从机

void slave_enable(void)
{
 SPI2.CS_REG &= ~0x1; //enable salve
 delay(3);
}

3.1 发送数据

void send_byte(unsigned char data)
{
 SPI2.CH_CFG |= 0x1; // enable Tx Channel
 delay(1);
 SPI2.SPI_TX_DATA = data;
 while( !(SPI2.SPI_STATUS & (0x1 << 25)) );
 SPI2.CH_CFG &= ~0x1; // disable Tx Channel
}

3.2 接收数据

unsigned char recv_byte()
{
 unsigned char data;
 SPI2.CH_CFG |= 0x1 << 1; // enable Rx Channel
 delay(1);
 data = SPI2.SPI_RX_DATA;
 delay(1);
 SPI2.CH_CFG &= ~(0x1 << 1); //disable Rx Channel
 return  data;
}

取消片选

void slave_disable(void)
{

 SPI2.CS_REG |= 0x1; //disable salve
 delay(1);
}

OK,到底为止,如何通过 SPI 收发数据,我们已经可以实现了,但是 SPI 往往外部回接各种各样的从设备。下面我们来看 SPI 如何操作从设备。

上面我们说了,fs4412 开发板的 SPI2 外设外接了 MCP2515,现在我们来分析一下 MCP2515。

MCP2515

MCP2515 详细资料,大家自行搜索 MCP2515 datasheet 《带有 SPI 接口的独立 CAN 控制器》。

简介

MCP2515 是一种独立的 CAN 总线通信控制器,是 Microchip 公司首批独立 CAN 解决方案的升级器件,其传输能力较 Microchip 公司原有 CAN 控制器(MCP2510)高两倍,高通信速率可达到 1Mbps。MCP2515 能够接收和发送标准数据帧和扩展数据帧以及远程帧,通过两个接收屏蔽寄存器和六个接收过滤寄存器滤除无关报文,从而减轻 CPU 负担。

特性

MCP2515 主要功能参数及电气特性如下:

  • (1)支持 CAN 技术规范 2.0A/B, 高传输速率达到 1Mbps;(2)支持标准数据帧、扩展数据帧和远程帧,每帧数据域长度可为 0~8 个字节;(3)内含两个的接收缓冲器和三个发送缓冲器,并且可编程设定优先级;(4)内含六个 29 位(bit)的接收过滤寄存器和两个 29 位(bit)的接收屏蔽寄存器;(5)高速 SPI 接口,支持 SPI 0,0 和 1,1 模式;(6)一次性模式可确保报文被一次性传输;(7)具有可编程时钟脉冲输出引脚,可作为其他芯片时钟信号源;(8) 帧起始(SOF)信号输出功能可被用于在确定的系统中(如时间触发 CAN-TTCAN)执行时隙功能,或在 CAN 总线诊断中决定早期总线出级;(9) 采用低功耗 CMOS 技术,工作电压:2.7V~5.5V, 工作电流:5mA(待机状态 1μA);(10)工作温度范围:(I)-40℃到+85℃,(E)-40℃到+125℃。

结构框图

CAN 模块

MCP2515 是一款独立 CAN 控制器, 可简化需要与 CAN 总线连接的应用。如上图 简要显示了 MCP2515 的结构框图。该器件主要由三个部分组成:

CAN 模块,包括 CAN 协议引擎、验收滤波寄存 器、验收屏蔽寄存器、发送和接收缓冲器。用于配置该器件及其运行的控制逻辑和寄存器。SPI 协议模块。

 

CAN 模块的功能是处理所有 CAN 总线上的报文接收和发送。报文发送时,首先将报文装载到正确的报文缓冲器和控制寄存器中。通过 SPI 接口设置控制寄存器中的相应位或使用发送使能引脚均可启动发送操作。通过读取相应的寄存器可以检查通讯状态和错误。会对在 CAN 总线上检测到的任何报文进行错误检查,然后与用户定义的滤波器进行匹配,以确定是否将报文移到两个接收缓冲器中的一个。

 

SPI 协议模块

MCU 通过 SPI 接口与该器件连接。使用标准的 SPI 读 / 写指令以及专门的 SPI 命令来读 / 写所有的寄存器。MCP2515 设计可与许多单片机的串行外设接口( SPI)直接相连,支持 0,0 和 1,1 运行模式。外部数据和命令通过 SI 引脚传送到器件中,且数据在 SCK 时钟信号的上升沿传送进去。MCP2515 在 SCK 的下降沿通过 SO 引脚传送出去。在进行任何操作时, CS 引脚都必须保持为低电平。

 

与 MCP2515 通信

SPI 指令集

我们要想操作 MCP2515,只能用过 SPI 总线向 MCP2515 发送一些数据,根据规定,发送不同的数据就代表不同的操作,于是就形成了对应的指令集。

 

如上图所示,比如我们要执行复位操作,那么我只需要通过 SPI 总线写入 11000000,即 0xC0 即可。

 

下面我们来详细分析如何向 MCP2515 发送复位命令,如何读写数据。

 

复位

因为复位只需要发送 0XC0 即可,并不需要其他额外操作,所以我们只需要根据我们之前章节所讲的 SPI 发送数据流程操作接口。

void reset_2515()
{
 soft_reset();      // 复位 spi 控制器
    slave_enable() ;   // 片选从机
 send_byte(0xc0);   // 发送复位命令
 slave_disable() ;  // 取消片选

}

读取数据

 

上图可知,读取数据流程如下:

片选从机通过 SPI 发送指令 0x03 发送地址读取数据取消片选

 

unsigned char read_byte_2515(unsigned char Addr)
{
 unsigned char ret;
    slave_enable();
    send_byte(0x03);
    send_byte(Addr);
    ret = recv_byte();
    slave_disable();
    return(ret);
}

 

发送数据

 

由上图可知,发送数据流程如下:

  1. 片选从机通过 SPI 发送指令 0x02 发送地址发送数据取消片选
  2.  
void write_byte_2515(unsigned char addr,unsigned char data)
{
    slave_enable();
    send_byte(0x02);
    send_byte(addr);
    send_byte(data);
    slave_disable();
}

 

读 RX 缓冲器

装载 TX 缓冲器

 

请求发送(RTS)指令

位修改指令

 

有些寄存器要修改对应的 bit,必须先发送该指令,发送屏蔽字节,然后才可以修改屏蔽字节中对应位为 1 的值,后面会有例子给出。

 

CAN

知道该如何和 CAN 通信,下面我们来看如何初始化 CAN。

 

CAN 初始化

CAN 的初始化步骤如下:

  1. MCP2515 复位设置 MCP2515 为配置模式位定时配置,有配置寄存器 CNF1,CNF2,CNF3 控制中断使能接收缓冲器配置引脚控制寄存器和状态寄存器

此外为方便测试,我们再加一步:回环模式,用于测试

 

1. MCP2515 复位

上一节已经实现。

void reset_2515()

2. 设置 MCP2515 为配置模式

 

 

由上图所示,只需要向地址 0X0F 写入数据 0x80 即可。

 write_byte_2515(0x0f, 0x80);    //CANCTRL 寄存器--进入配置模式 中文 DATASHEET 58 页

 

3. 位定时配置,有配置寄存器 CNF1,CNF2,CNF3 控制

CAN 总线上的所有节点都必须具有相同的标称比特率。CAN 协议采用不归零( Non Return to Zero, NRZ)编码方式,在数据流中不对时钟信号进行编码。因此,接收时钟信号必须由接收节点恢复并与发送器的时钟同步。

 

由于不同节点的振荡器频率和传输时间不同,接收器应具有某种能与数据传输边沿同步的锁相环( Phase Lock Loop, PLL)来同步时钟并保持这种同步。

 

鉴于数据采用 NRZ 编码,有必要进行位填充以确保至少每 6 位时间发生一次边沿,使数字锁相环 ( Digital Phase LockLoop, DPLL)同步。MCP2515 通过 DPLL 实现位定时。DPLL 被配置成同输入数据同步,并为发送数据提供标称定时。DPLL 将每一 个 位 时 间 分 割 为 由 最 小 单 位 为 时 间 份 额 ( Time Quanta, TQ)所组成的多个时间段。

 

在位时间帧中执行的总线定时功能,例如与本地振荡器同步、网络传输延迟补偿和采样点定位等,都是由 DPLL 的可编程位定时逻辑来规定的。

 

CNF1BRP<5:0> 控制波特率预分频比的设置。这些位根据 OSC1 输入频率设置 TQ 的时间长度。当 BRP<5:0> =‘b000000’, TQ 最小值取 2 TOSC。通过 SJW<1:0> 选择以 TQ 计的同步跳转宽度。

 

CNF2PRSEG<2:0> 位设定以 TQ 计的传播段时间长度。PHSEG1<2:0>位设定以 TQ 计的相位缓冲段 PS1 的时间长度。

 

CNF3 如果 CNF2.BTLMODE 位为 1,则相位缓冲段 PS2 的时间长度将由 PHSEG2<2:0> 位设定,以 TQ 计。如果 BTLMODE 位为 0,则 PHSEG2<2:0> 位不起作用。

 

 

MCP2515 波特率配置以 16M 晶振为例,     SJW  段数(1+PRSEG+PRSEG1+PRSEG2)

#define CNF1_20K     0xd3        //4   20(1+4+8+7) 
#define CNF2_20K     0xfb          
#define CNF3_20K     0x46           

// 可以设置的波特率 5K 10K 15K 20K 25K 40K 50K 80K 100K 125K 200K 400K 500K 667K 800K 1M
write_byte_2515(0x2A, CNF1_20K); //CNF1 位定时配置寄器  
write_byte_2515(0x29, CNF2_20K); //CNF2 位定时配置寄器 
write_byte_2515(0x28, CNF3_20K); //CNF3 位定时配置寄器 

 

4. 中断使能

MCP2515 有八个中断源。CANINTE 寄存器包含了使能各中断源的中断使能位。CANINTF 寄存器包含了各中断源的中断标志位。当发生中断时, INT 引脚将被 MCP2515 拉为低电平, 并保持低电平状态直至 MCU 清除中断。中断只有在引起相应中断的条件消失后,才会被清除。建议在对 CANINTF 寄存器中的标志位进行复位操作时,采用位修改命令而不要使用一般的写操作。这是为了避免在写命令执行过程中无意间修改了标志位,进而导致中断丢失。应该注意的是, CANINTF 中的中断标志位是可读写位,因此在相关 CANINTE 中断使能位置 1 的前提下,对上述任一位置 1 均可使 MCU 产生中断请求。

 

write_byte_2515(0x2B, 0x1f);     //CANINTE 中断使能寄存器  

 

5. 接收缓冲器配置

MCP2515 具有两个全接收缓冲器。每个接收缓冲器配备 有多 个 验 收滤 波 器。除 上述 专 用 接收 缓 冲 器外,MCP2515 还具有单独的报文集成缓冲器 ( Message Assembly Buffer, MAB) ,可作为第三个接收缓冲器。

 

bit5:6 设置为 1,其余位暂时不用,设置为 0.

 write_byte_2515(0x60, 0x60);   //RXB0CTRL 接收缓冲器 0 控制寄存器 

 

6. 引脚控制寄存器和状态寄存器

当引脚配置为数字输出引脚时,相应的接收缓冲器中的 BFPCTRL.BxBFM 位应被清零, 而 BFPCTRL.BnBFE 位应被置 1。在这种工作模式下,引脚的状态由 BFPCTRL.BnBFS 位控制。BnBFS 位写入 1 时,将使相应的缓冲器满中断引脚输出高电平,写入 0 将使该引脚输出低电平。当引脚处于这种模式时,该引脚的状态只应通过位修改 SPI 命令来修改,以避免任何缓冲器满中断引脚出现干扰。

void bit_modify_2515(unsigned char addr, unsigned char mask, unsigned char data)
{
//    CS_SPI = 0 ;
    slave_enable() ;
    send_byte(0x05) ;
    send_byte(addr) ;
    send_byte(mask) ;
    send_byte(data) ;
    slave_disable() ;
//    CS_SPI = 1 ;
}
bit_modify_2515(0x0C, 0x0f, 0x0f); //BFPCTRL_RXnBF 引脚控制寄存器和状态寄存器 中文 DATASHEET 29 页

7. 回环模式,用于测试

write_byte_2515(0x0f, 0x40);   //CAN 控制寄存器--回环模式,用于测试

 

can 缓冲区数据收发

MCP2515 采用三个发送缓冲器。每个发送缓冲器占用 14 字节的 SRAM,并映射到器件存储器中。其中第一个字节 TXBnCTRL 是与报文缓冲器相关的控制寄存器。该寄存器中的信息决定了报文在何种条件下发送,并在报文发送时指示其状态 。

 

用 5 个字节来装载标准和扩展标识符以及其他报文仲裁信息(见寄存器 3-3 到寄存器 3-7) 。最后 8 个字节用于装载等待发送报文的 8 个可能的数据字节 。

 

至少须将 TXBnSIDH、 TXBnSIDL 和 TXBnDLC 寄存器装载数据。如果报文包含数据字节,还需要对 TXBnDm 寄存器进行装载。若报文采用扩展标识符,应对 TXBnEIDm 寄存器进行装载,并将 TXBnSIDL.EXIDE 位置 1。

 

下面我们看如何向 CAN 的缓冲区 0 发送和接收数据。

 

数据发送

can 缓冲区 0 数据发送流程如下:

  1. 设置为发送最高优先级设置发送缓冲器 0 标准标识符高位设置发送缓冲器 0 标准标识符低位设置发送缓冲器 0 数据长度码 8 字节向缓冲区写入数据,地址从 0x36 起发送请求命令 0x81,发送数据

 

1. 设置为发送最高优先级

TXBnCTRL

write_byte_2515(0x30, 0x03); // 设置为发送最高优先级

 

2. 设置发送缓冲器 0 标准标识符高位

write_byte_2515(0x31, 0xff); // 发送缓冲器 0 标准标识符高位

 

3. 设置发送缓冲器 0 标准标识符低位

write_byte_2515(0x32, 0x00); // 发送缓冲器 0 标准标识符低位

 

4. 设置发送缓冲器 0 数据长度码 8 字节

write_byte_2515(0x35, 0x08);  // 发送缓冲器 0 数据长度码 8 字节

 

5. 向缓冲区写入数据,地址从 0x36 起

write_byte_2515(0x36+i ,tx_buff[i]); // 向 txb 缓冲器中写入 8 个字节

 

6. 发送请求命令 0x81,发送数据

void send_req_2515()
{
 //   CS_SPI = 0; // 复位
 soft_reset();      // 复位 spi 控制器
    slave_enable() ;   // 片选从机
 send_byte(0x81);   // 发送请求命令
 slave_disable() ;  // 取消片选
// CS_SPI=1;
}

Can 数据的接收

从 CAN 缓冲区读取数据流程如下:

  1. 读取中断标志寄存器 0x2c 的 value,判断 bit0 是否为 1 从接收缓冲区读走数据,地址从 0X66 开始软复位向中断标志寄存器 0x2c 写入位掩码向中断标志寄存器 0x2c,写入数据 0,清终端

1. 读取中断标志寄存器 0x2c 的 value,判断 bit0 是否为 1

当报文传送至某一接收缓冲器时,与该接收缓冲器对应的 CANINTF.RXnIF 位将置 1。一旦缓冲器中的报文处理完毕, MCU 就必须将该位清零以接收下一条报文。该控制位提供的锁定功能确保 MCU 尚未处理完上一条报文前, MCP2515 不会将新的报文载入接收缓冲器。

 

如果 CANINTE.RXnIE 位被置 1,器件会在 INT 引脚产生一个中断,显示接收到报文有效。另外,如果被配置为接收缓冲器满中断引脚,与之相应的 RXnBF 引脚会被拉低。

 

MCP2515 有八个中断源。CANINTE 寄存器包含了使能各中断源的中断使能位。CANINTF 寄存器包含了各中断源的中断标志位。当发生中断时, INT 引脚将被 MCP2515 拉为低电平, 并保持低电平状态直至 MCU 清除中断。中断只有在引起相应中断的条件消失后,才会被清除。

 

建议在对 CANINTF 寄存器中的标志位进行复位操作时,采用位修改命令而不要使用一般的写操作。这是为了避免在写命令执行过程中无意间修改了标志位,进而导致中断丢失。

 

应该注意的是, CANINTF 中的中断标志位是可读写位,因此在相关 CANINTE 中断使能位置 1 的前提下,对上述任一位置 1 均可使 MCU 产生中断请求。

 

 

2. 从接收缓冲区读走数据

rx_buff[i]= read_byte_2515(0x66+i);

3. 软复位

soft_reset();

4. 向中断标志寄存器 0x2c 写入位掩码

bit_modify_2515(0x2c,0x01,0x00);// 修改 bit 0

5. 清中断

write_byte_2515(0x2c, 0x00);

 

最终操作代码如下

节省篇幅,重复函数不贴了。

#define CNF1_20K     0xd3        //4   20(1+4+8+7) 
#define CNF2_20K     0xfb          
#define CNF3_20K     0x46  
         
void  Init_can(void)
{
    reset_2515(); // 复位
    write_byte_2515(0x0f, 0x80); //CANCTRL 寄存器--进入配置模式 中文 DATASHEET 58 页
 // 可以设置的波特率 5K 10K 15K 20K 25K 40K 50K 80K 100K 125K 200K 400K 500K 667K 800K 1M
    write_byte_2515(0x2A, CNF1_20K); //CNF1 位定时配置寄器   中文 DATASHEET 41-42 页
    write_byte_2515(0x29, CNF2_20K); //CNF2 位定时配置寄器   中文 DATASHEET 41-42 页
    write_byte_2515(0x28, CNF3_20K); //CNF3 位定时配置寄器   中文 DATASHEET 41-43 页
    write_byte_2515(0x2B, 0x1f);     //CANINTE 中断使能寄存器  中文 DATASHEET 50 页
    write_byte_2515(0x60, 0x60);     //RXB0CTRL 接收缓冲器 0 控制寄存器 中文 DATASHEET 27 页
    //write_byte_2515(0x70, 0x20);   // 接收缓冲器 1 控制寄存器
    bit_modify_2515(0x0C, 0x0f, 0x0f); //BFPCTRL_RXnBF 引脚控制寄存器和状态寄存器 中文 DATASHEET 29 页
    write_byte_2515(0x0f, 0x40);   //CAN 控制寄存器--回环模式,用于测试
}
void send_byte(unsigned char data)
{
 SPI2.CH_CFG |= 0x1; // enable Tx Channel
 delay(1);
 SPI2.SPI_TX_DATA = data;
 while( !(SPI2.SPI_STATUS & (0x1 << 25)) );
 SPI2.CH_CFG &= ~0x1; // disable Tx Channel
}
unsigned char recv_byte()
{
 unsigned char data;
 SPI2.CH_CFG |= 0x1 << 1; // enable Rx Channel
 delay(1);
 data = SPI2.SPI_RX_DATA;
 delay(1);
 SPI2.CH_CFG &= ~(0x1 << 1); //disable Rx Channel
 return  data;
}
void bit_modify_2515(unsigned char addr, unsigned char mask, unsigned char data)
{
//    CS_SPI = 0 ;
    slave_enable() ;
    send_byte(0x05) ;
    send_byte(addr) ;
    send_byte(mask) ;
    send_byte(data) ;
    slave_disable() ;
//    CS_SPI = 1 ;
}
void Can_send(unsigned char *tx_buff)
{
 unsigned char i;
 write_byte_2515(0x30, 0x03); // 设置为发送最高优先级
 write_byte_2515(0x31, 0xff); // 发送缓冲器 0 标准标识符高位
 write_byte_2515(0x32, 0x00); // 发送缓冲器 0 标准标识符低位
 write_byte_2515(0x35, 0x08);  // 发送缓冲器 0 数据长度码 8 字节
 for(i = 0; i < 8; i++)
 {
  write_byte_2515(0x36+i ,tx_buff[i]); // 向 txb 缓冲器中写入 8 个字节
//  printf("%x ",tx_buff[i]);
 }
 send_req_2515();

}

unsigned char Can_receive(unsigned char *rx_buff)
{
 unsigned char i,flag;
    flag = read_byte_2515(0x2c); //CANINTF——中断标志寄存器
    printf("flag=%x\n",flag);
  //  printf(" SPI2.SPI_STATUS =%x\n", SPI2.SPI_STATUS );
 //   soft_reset();
    if (flag&0x1)                // 接收缓冲器 0 满中断标志位
    {
     for(i = 0; i < 16; i++)
  {
      rx_buff[i]= read_byte_2515(0x66+i);
    //  printf("%x ",rx_buff[i]);
    //  printf(" SPI2.SPI_STATUS =%x\n", SPI2.SPI_STATUS );
      soft_reset();
  }
     bit_modify_2515(0x2c,0x01,0x00);
     write_byte_2515(0x2c, 0x00);
  if (!(rx_buff[1]&0x08)) return(1);   // 接收标准数据帧
    }
    return(0);
}
int main(void)
{
 GPX2.CON = 0x1 << 28;
 uart_init();

 unsigned char ID[4],buff[8];                 // 状态字
 unsigned char key;
 unsigned char ret;//,j,k,ret0,ret1,ret2,ret3,ret4,ret5,ret6,ret7,ret8,ret9;
 unsigned int rx_counter;
 volatile int i=0;

 GPC1.CON = (GPC1.CON & ~0xffff0) | 0x55550;// 设置 IO 引脚为 SPI 模式

/*spi clock config*/
 CLK_SRC_PERIL1 = (CLK_SRC_PERIL1 & ~(0xF<<24)) | 6<<24;// 0x6: 0110 = SCLKMPLL_USER_T 800Mhz
 CLK_DIV_PERIL2 = 19 <<8 | 3;//SPI_CLK = 800/(19+1)/(3+1)

 soft_reset();                    // 软复位 SPI 控制器
 SPI2.CH_CFG &= ~( (0x1 << 4) | (0x1 << 3) | (0x1 << 2) | 0x3);//master mode, CPOL = 0, CPHA = 0 (Format A)
 SPI2.MODE_CFG &= ~((0x3 << 17) | (0x3 << 29));   //BUS_WIDTH=8bit,CH_WIDTH=8bit
 SPI2.CS_REG &= ~(0x1 << 1);        // 选择手动选择芯片
 mydelay_ms(10);    // 延时
    Init_can();   // 初始化 MCP2515

    printf("\n************ SPI CAN test!! ************\n");

    while(1)
    {
  //Turn on led
  GPX2.DAT = GPX2.DAT | 0x1 << 7;
  mydelay_ms(50);

     printf("\nplease input 8 bytes\n");

     for(i=0;i<8;i++)
     {
      src[i] = getchar();
      putc(src[i]);
     }
     printf("\n");

     Can_send(src); // 发送标准帧
        mydelay_ms(100);
        ret = Can_receive(dst); // 接收 CAN 总线数据
        printf("ret=%x\n",ret);
        printf("src=");
        for(i=0;i<8;i++) printf(" %x", src[i]);// 将 CAN 总线上收到的数据发到串行口

        printf("\n");

        printf("dst=");
        for(i=0;i<8;i++) printf(" %x",dst[6+i]); // 将 CAN 总线上收到的数据发到串行口
  printf("\n");

  //Turn off
  GPX2.DAT = GPX2.DAT & ~(0x1 << 7);
  mydelay_ms(100);
    } //while(1)

 return 0;
} //main