查看原文
其他

18.基于Cortex-A9 SPI、MCP2515详解

土豆居士 一口Linux 2021-11-05

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 Data Tansfer

上图只是对 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的时候;

本博文采用如下用法:

  1. 极性=CPOL
  2. 相位=CPHA
  3. SCLK=时钟
  4. 第一个边沿和第二个边沿

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

模式极性 相位
Mode 1CPOL=0,CPHA=0
Mode 2CPOL=0,CPHA=1
Mode 3CPOL=1,CPHA=0
Mode 4CPOL=1,CPHA=1
采样示意图

CPOL=0,CPHA=0

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

CPOL=0,CPHA=0

CPOL=0,CPHA=1

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

CPOL=0,CPHA=1

CPOL=1,CPHA=0

脉冲传输前和完成后都保持在高电平状态,所以 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模式;
  2. 设置clock;
  3. 软件复位;
  4. 设置CPOL CPHA  为 00 模式,并设置为主机模式;
  5. 设置数据位;
  6. 片选。

1. 设置GPIO引脚为SPI模式

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

GPC1

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

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复位
  2. 片选从机
  3. 收发数据
  4. 取消片选

【注意】一下代码为设置每次只读写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 的结构框图。该器件主要由三个部分组成:

  1. CAN 模块,包括 CAN 协议引擎、验收滤波寄存 器、验收屏蔽寄存器、发送和接收缓冲器。
  2. 用于配置该器件及其运行的控制逻辑和寄存器。
  3. 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() ;  //取消片选

}

读取数据

读指令

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

  1. 片选从机
  2. 通过SPI发送指令0x03
  3. 发送地址
  4. 读取数据
  5. 取消片选
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. 片选从机
  2. 通过SPI发送指令0x02
  3. 发送地址
  4. 发送数据
  5. 取消片选
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复位
  2. 设置MCP2515为配置模式
  3. 位定时配置,有配置寄存器CNF1,CNF2,CNF3控制
  4. 中断使能
  5. 接收缓冲器配置
  6. 引脚控制寄存器和状态寄存器

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

1. MCP2515复位

上一节已经实现。

void reset_2515()

2. 设置MCP2515为配置模式

CANCTRL

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

 write_byte_2515(0x0f0x80);    //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 的可编程位定时逻辑来规定的。

「CNF1」

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

「CNF2」

PRSEG<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 产生中断请求。

CANINTE
write_byte_2515(0x2B0x1f);     //CANINTE中断使能寄存器  

5. 接收缓冲器配置

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

RXB0CTRL

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

 write_byte_2515(0x600x60);   //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(0x0C0x0f0x0f); //BFPCTRL_RXnBF 引脚控制寄存器和状态寄存器 中文DATASHEET 29 页

7. 回环模式,用于测试

write_byte_2515(0x0f0x40);   //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. 设置为发送最高优先级
  2. 设置发送缓冲器0标准标识符高位
  3. 设置发送缓冲器0标准标识符低位
  4. 设置发送缓冲器0数据长度码8字节
  5. 向缓冲区写入数据,地址从0x36起
  6. 发送请求命令0x81,发送数据

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

TXBnCTRL
write_byte_2515(0x300x03); //设置为发送最高优先级

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

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

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

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

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

write_byte_2515(0x350x08);  //发送缓冲器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
  2. 从接收缓冲区读走数据,地址从0X66开始
  3. 软复位
  4. 向中断标志寄存器0x2c写入位掩码
  5. 向中断标志寄存器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(0x2c0x00);

最终操作代码如下

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

#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(0x0f0x80); //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(0x2B0x1f);     //CANINTE中断使能寄存器  中文DATASHEET 50 页
    write_byte_2515(0x600x60);     //RXB0CTRL接收缓冲器0 控制寄存器 中文DATASHEET 27 页
    //write_byte_2515(0x70, 0x20);   //接收缓冲器1控制寄存器
    bit_modify_2515(0x0C0x0f0x0f); //BFPCTRL_RXnBF 引脚控制寄存器和状态寄存器 中文DATASHEET 29 页
    write_byte_2515(0x0f0x40);   //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(0x300x03); //设置为发送最高优先级
 write_byte_2515(0x310xff); //发送缓冲器0标准标识符高位
 write_byte_2515(0x320x00); //发送缓冲器0标准标识符低位
 write_byte_2515(0x350x08);  //发送缓冲器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(0x2c0x00);
  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



-END-


推荐阅读


【1】12. 如何基于Cortex-A9的UART详解 必读【2】网络/命令行抓包工具tcpdump详解 必读
【3】13.一文搞懂Cortex-A9 RTC 必读【4】DNS 原理入门指南
【5】14. 从0学ARM Cortex-A9 看门狗入门【6】apt 和 apt-get 之间有什么区别?
【7】16.从0学arm,基于Cortex-A9 ADC裸机驱动详解
【8】17.基于Cortex-A9,i2c 外设详解【9】【粉丝问答8】用C语言在Linux下实现CC2530上位机-1【10】CAN】嵌入式CAN总线入门篇(底层细节)

 


 

进群,请加一口君个人微信,带你嵌入式入门进阶。


在公众号内回复「1024」,即可免费获取学习资料,期待你的关注~


: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存