1)实验平台:定时原子开拓者FPGA开发版。
(2)从摘自《开拓者 Nios II开发指南》的官方微信号公众号中获取更多信息:定时原子。
3)完整实验源手册视频下载地址:
第20章CAN通信实验
现场总线技术是自动控制领域的后起之秀,成本低,容易利用现有的数字化和互联网。
网络技术的新成果具有改造系统等特点,顺应了当今时代数字化、模块化、网络化的发展。
朝向。CAN总线是现场总线系列中最有希望的现场总线之一,在汽车产业、工艺产业、机械工
在产业、机器人、建筑物自动化等领域发挥着重要作用。在本章中,您将学习使用CAN总线。本章介绍了
下一章:
20.1简介
20.2实验任务
20.3硬件设计
20.4软件设计
20.5下载验证
简介
20世纪80年代以来,汽车ECU越来越多,包括ABS、电控门、电子燃油喷射装置等。比如说,
如果仍然采用一般的点对点布线方式,即电线一端连接到开关,另一端与电气设备相通。
汽车的电线数量急剧增加,线束的冗余和维修成本增加了。此外,现在的蒸汽
汽车业界因对安全性、舒适性、便利性、低污染、低成本的要求,进行了多种电子控制
系统开发出来了。因为这些系统之间通信使用的数据类型和可靠性要求不同。
对汽车的线束分布及信息通信提出了更高的要求。CAN总线技术应运而生。
CAN是控制器区域网络(控制器LAN)的缩写,是ISO国际标准化的基于消息的
广播模式下的串行通信协议实时共享信息,传统布线方式下线束较多,布线可行。
困难、费用高的问题。德国汽车电子产品开发和生产著名的BOSCH开发并多次修改。
1991年9月,形成了由2.0A(11位标准帧格式)和2.0B(29位扩展)组成的技术规范版本2.0
框架)两部分。此后,CAN通过ISO11898和ISO11519进行了标准化,其中ISO11898是关于通信速度的。
这是125Kbps到1Mbps的高速通信标准,而ISO11519用于125Kbps以下的低速通信
标准。在欧洲,CAN已经是汽车网络的标准协议。
图20.1.1 CAN汽车网络拓扑结构
目前,CAN的高性能和可靠性已经得到承认,并广泛用于工业自动化、船舶、医疗设施。
准备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被称为自动化领域
计算机局域网。它的出现通过分布式控制系统,实现了节点之间实时、可靠的数据通信。
强大的技术支持。
CAN协议具有以下特征:
1)多主控制。总线空闲时,所有单元都可以发送消息(多主控制),3个以上的单个
元同时开始发送消息时,将根据标识符(称为标识符或以下ID)确定优先级。ID不是表示法
发送的大象地址表示访问总线的消息的优先级。两个或多个单元同时开始发送消息
对每个消息ID的每个位执行一次仲裁比较。仲裁胜利(被判定为优先级最高的单位)
如果继续发送信息,仲裁失败的单位将立即停止发送,进行接收工作。
2)系统的灵活性。连接到总线的单元没有类似于“地址”的信息。因此,向总线添加单个
从元连接到总线的其他设备的硬件和软件以及应用层不需要更改。
3)实时性能、远距离传输、快速通信速度(最高1Mbps(距离小于40米)、最高
最长10公里(5Kbps以下),抗电磁干扰能力强,成本节约优势。
4)提供错误检测、错误通知和错误恢复功能。检测所有单元中的错误(故障检测功能)
是的,检测到错误的单元立即向所有其他单元(错误通知功能)发送消息
如果单元检测到错误,则强制终止当前传输。结束强制关闭的设备将继续重新发送
此消息将一直发送到成功发送为止(故障恢复功能)。
5)关闭故障的功能。CAN可以看出,错误的类型是总线上的临时数据。
错误(如外部噪声
等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上
发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。
6)连接节点多。CAN总线是可同时连接多个单元的总线。可连接的单元总数理论上是没
有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速
度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
正是因为CAN协议的这些特点,使得CAN特别适合工业过程监控设备的互连,因此,越来
越受到工业界的重视,并已公认为最有前途的现场总线之一。
下面我们从CAN的基本概念入手,对CAN有一个基本的了解。
一、 总线拓扑图
CAN网络的拓扑一般为线型。线束最常用的是双绞线,其中一根线称为CAN_H,另一根线
称为CAN_L,两根线上传输的是对称的差分电平信号。下图为CAN总线网络示意图:
图 20.1.2 CAN总线网络示意图
CAN总线网络挂在CAN_H和CAN_L,连接在CAN总线上的设备叫做节点设备(CAN Node),
各个节点通过这两条线实现信号的串行差分传输,为了避免信号的反射和干扰,还需要在
CAN_H和CAN_L之间接上120Ω的终端电阻,来做阻抗匹配,以减少回波反射。节点设备主要包
括MCU、控制器和收发器。MCU常集成有CAN控制器,CAN控制器负责处理协议相关功能,以减
轻MCU的负担。CAN收发器将数据传到总线或者从总线接收数据给控制器。
图 20.1.3 120Ω的终端电阻
注:为什么使用120Ω的终端电阻?任何一根线缆的特征阻抗都可以通过实验的方式得
出。线缆的一端接方波发生器,另一端接一个可调电阻,并通过示波器观察电阻上的波形。
调整电阻阻值的大小,直到电阻上的信号是一个良好的无振铃的方波,此时的电阻值可以认
为与线缆的特征阻抗一致。大部分汽车线缆都是单线的,如果采用两根汽车使用的典型线
缆,将它们扭制成双绞线,就可根据上述方法得到特征阻抗大约为120Ω,这也是CAN标准推
荐的终端电阻阻值。
CAN总线分高速CAN和低速CAN,收发器也分为高速CAN收发器(1Mbps)和低速CAN收发器
(125Kbps)。低速CAN也叫Fault Tolerance CAN,指的是即使总线上一根线失效,总线依然
可以通信。我们开拓者使用的TJA1050为高速CAN收发器。
图 20.1.4 CAN收发器
在发送数据时,CAN控制器把要发送的二进制编码通过TXD引脚发送到CAN收发器,然后由
收发器把这个普通的逻辑电平信号转化成差分信号,通过差分线CAN_H和CAN_L输出到CAN总线
网络。接收数据过程则相反。CAN收发器具体的管脚定义如下:
图 20.1.5 管脚定义
二、 CAN 信号表示
CAN总线采用差分信号传输,通常情况下只需要两根信号线就可以进行正常的通信。在差
分信号中,逻辑0和逻辑1是用两根差分信号线的电压差来表示。当处于逻辑1,CAN_H和CAN_L
的电压差小于0.5V,称为隐性电平(Recessive);当处于逻辑0,CAN_H和CAN_L的电压差大
于0.9V,称为显性电平(Dominant)。
图 20.1.6 高速CAN总线差分信号
典型地,CAN总线为隐性(逻辑1)时,CAN_H和CAN_L的电平都为2.5V(电位差为0V);
CAN总线为显性(逻辑0)时,CAN_H和CAN_L电平分别为3.5V和1.5V(电位差为2.0V)。在总
线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。而隐形电
平则具有包容的意味,只有所有的单元都输出隐性电平,总线上才为隐性电平。
三、 CAN 协议的帧格式
CAN网络可以配置为使用两种不同的帧格式:标准帧格式(在CAN 2.0A和CAN2.0 B中描
述)和扩展帧格式(仅由CAN2.0 B描述))。两种格式之间的唯一区别是“CAN标准帧”支持
标识符的11位长度,“CAN扩展帧”支持标识符的29位长度,由11位标识符组成(“标准标识
符”)和18位扩展名(“扩展标识符”)。CAN标准帧格式和CAN扩展帧格式之间的区别是通
过使用IDE位来实现的,该位在11位帧的情况下作为显性发送,并且在29位帧的情况下作为隐
性发送。支持扩展帧格式消息的CAN控制器也能够以CAN标准帧格式发送和接收消息。所有帧
都以帧起始(SOF)位开始,该位表示帧传输的开始。
CAN有四种帧类型:
◆ 数据帧:发送单元向接收单元传送数据的帧
◆ 远程帧:总线单元发出远程帧,请求发送具有同一识别符的数据帧
◆ 错误帧:由检测到错误的任何节点发送的帧
◆ 过载帧:在数据或远程帧之间注入延迟的帧
由于篇幅所限,我们这里仅对数据帧进行详细介绍,
数据帧是唯一实际传输数据的帧,结构上由7个段组成,其中根据仲裁段ID码长度的不
同,分为标准帧(CAN2.0A)和扩展帧(CAN2.0B):
➢ 标准帧格式:具有 11 个标识符位
➢ 扩展帧格式:具有 29 个标识符位
标准数据帧的构成如下图所示:
图 20.1.7 CAN的标准数据帧
数据帧一般由7个段构成,即:
(1)帧起始:表示数据帧开始的段。
(2)仲裁段:表示该帧优先级的段。
(3)控制段:表示数据的字节数及保留位的段。
(4)数据段:数据的内容,一帧可发送0~8个字节的数据。
(5)CRC段:检查帧的传输错误的段。
(6)ACK段:表示确认正常接收的段。
(7)帧结束:表示数据帧结束的段。
对于标准数据帧,格式如下(长度为bit,排列顺序为发送顺序):
图 20.1.8 标准数据帧格式
对于扩展数据帧,格式如下
图 20.1.9 扩展数据帧格式
标准帧与扩展帧的区别段如下图所示:
图 20.1.10 标准帧与扩展帧的区别段
下面我们具体进行介绍。
帧起始和帧结束:帧起始和帧结束用于界定一个数据帧,无论是标准数据帧还是扩展数
据帧都包含这两个段。
图 20.1.11 数据帧的帧起始和帧结束
仲裁段:仲裁段的内容主要为本数据帧的ID信息。在CAN协议中,ID决定着数据帧发送的
优先级,也决定着其它设备是否会接收这个数据帧。数据帧的标准格式和扩展格式的主要区
别就在于ID信息的长度:标准格式的ID为11位;扩展格式为29位,具体区别如下图所示:
图 20.1.12 数据帧仲裁段构成
标准格式的ID有11个位。其中RTR位用于标识是否是远程帧(0,数据帧;1,远程帧),
IDE位为标识符选择位(0,使用标准标识符;1,使用扩展标识符),SRR位为代替远程请求
位,为隐性位,它代替了标准帧中的RTR位。
控制段:由6个位构成,表示数据段的字节数。标准帧和扩展帧的控制段稍有不同,如下
图所示:
图 20.1.13 数据帧控制段构成
上图中,r0和r1为保留位,必须全部以显性电平发送,但是接收端可以接收显性、隐性及任意组合的电平。DLC段为数据长度表示段,高位在前,DLC段有效值为0~8,但是接收方接
收到9~15的时候并不认为是错误,另外DLC服从BCD8421编码。
数据段:该段可包含0~8个字节的数据。从最高位(MSB)开始输出,标准帧和扩展帧在
这个段的定义都是一样的。如下图所示:
图 20.1.14 数据帧数据段构成
CRC段:该段用CRC校验检查帧传输错误,CRC段由15位的CRC校验值和1位CRC界定符构
成,如下图所示:
图 20.1.15 数据帧CRC段构成
此段CRC的值计算范围包括:帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算CRC值并进行比较,不一致时会通报错误。
ACK段:在该段发送节点发送两个隐性电平位,所有接收到匹配CRC序列(CRC
SEQUENCE)的节点即正确接收到有效的报文的节点会在ACK槽(ACK Slot)期间用一显性电平位
写入发送器的“隐性”位来作出应答。标准帧和扩展帧在这个段的格式也是相同的。如下图
所示:
图 20.1.16 数据帧CRC段构成
ACK槽也称为应答间隙。
ACK界定符:ACK界定符是ACK段的第二位,并且是一个必须为“隐性”的位。因此,应答
间隙(ACK SLOT)被两个“隐性”的位所包围,也就是CRC界定符和ACK界定符。
至此,数据帧的7个段就介绍完了,其他帧的介绍,请大家参考光盘的《CAN入门书》相
关章节。接下来,我们再来看看CAN的仲裁功能是如何实现的。
在总线空闲态,总线上任何节点都可以发送报文,最先开始发送消息的单元获得发送
权。如果有两个或两个以上的节点开始传送报文,那么就会存在总线访问冲突的可能。CAN使
用了标识符的逐位仲裁方法解决了这个问题。
当多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显
性电平最多的单元可继续发送。实现过程,如下图所示:
图 20.1.17 总线仲裁图
在仲裁期间,每一个发送器都对发送的电平与被监控的总线电平进行比较。如果电平相
同,则这个单元可以继续发送。如果发送的是一"隐性"电平而监视到的是一"显性"电平,那
么这个节点失去了仲裁,必须退出发送状态。
上图中,节点A、B、C同时开始向总线发送数据,开始部分他们的数据格式是一样的,故
无法区分优先级,直到ID的第5位,节点B输出隐性电平,而节点A、C输出显性电平,此时节
点B仲裁失利,立刻转入接收状态工作(只听模式),不再与节点A、C竞争,而节点A、C则继
续仲裁,在ID的第3位节点A输出显性电平,节点C输出隐性电平,此时节点C仲裁失利,立刻转入
接收状态工作,不再与节点A竞争,节点A顺利获得总线使用权,继续发送自己的数据。这就实现
了仲裁,让连续发送显性电平多的单元获得总线使用权。具体的数字化表示如下表:
图 20.1.18 仲裁的数字化表示
从上表我们可以看到,帧ID越小的节点优先级越高;CAN总线采用"线与"的规则进行总
线冲裁,即1&0=0,所以0为显性。同理,由于标准帧的IDE位为显性电平,扩展帧的IDE位为
隐性电平,对于前11位ID相同的标准帧和扩展帧,标准帧的优先级比扩展帧高。
图 20.1.19 前11位相同ID的标准帧比扩展帧优先级高
通过以上介绍,我们对CAN总线有了个大概了解,详细介绍参考光盘的:《CAN入门
书.pdf》。
四、 CAN 总线与 RS485 的比较
CAN总线与RS485都是采用差分信号进行传输,这两者有什么区别呢?
1) 速度与距离:CAN 与 RS485 以 1Mbps 的高速率传输的距离都不超过 100M,可谓高速
上的距离差不多。但是低速 CAN 以 5Kbps 时,距离可达 10KM。而增强型 RS485 收发
器在最低的速率时亦能传输超过 10KM(都无中继),两者在长距离的传输上也难分
伯仲;
图 20.1.20 楼宇自动化
2) 总线利用率:RS485 是单主从结构,就是一个总线上只能有一台主机,通讯都由它发
起的。它没有下命令,下面的节点不能发送,而且要发完即答,受到答复后,主机才
向下一个节点询问,这样是为了防止多个节点向总线发送数据,而造成数据错乱。而
CAN 是多主从结构,每个节点都有 CAN 控制器,多个节点发送时,以发送的 ID 号自
动进行仲裁,这样就可以实现总线数据不错乱,而且一个节点发送完,另一个节点探
测到总线空闲,而马上发送,这样省去了主机的询问,提高了总线利用率,增强了快
速性。所以在汽车等实性要求高的系统,都是用 CAN 总线,或者其他类似的总线;
3) 错误检测机制:RS485 只规定了物理层,而没有数据链路层,所以它对错误是无法识
别的,除非一些短路等物理错误。这样容易造成一个节点破坏了,拼命向总线发数据
(一直发 1),这样造成整个总线瘫痪。所以 RS485 一旦坏一个节点,这个总线网络
都瘫痪。而 CAN 总线有 CAN 控制器,可以对总线任何错误进行检测,自动转换错误
状态,适时关闭总线,进而保护总线。如果检测到其他节点错误或者自身错误,都会
向总线发送错误帧,来提示其他节点。这样 CAN 总线一旦有一个节点程序跑飞了,它
的控制器自动闭锁,保护总线。所以在安全性要求高的网路,CAN 是较好的选择;
4) 器件价格:随着 CAN 总线迅猛发展,目前 CAN 隔离收发器单价大有与 RS485 价格持
平的趋势,RS485 收发器逐渐失去价格优势;
5) 开发难度:CAN 具有完善的通信协议,底层机制由 CAN 控制器芯片及其接口芯片来实现,研发工程师只需要了解面向客户的应用层,从而大大降低了系统的开发难度,缩
短了开发周期。而 RS485 协议仅仅只有电气协议,客户开发需要自己开发链路层和应
用层,开发难度较大。
总结对比:
图 20.1.21 CAN与RS485的比较
实验任务
本章我们的实验任务是:使用两块开拓者开发板实现CAN接口通信功能,并把接收到的数
据显示在数码管上。
硬件设计
在前面的简介部分我们知道一个CAN节点除了需要CAN收发器外还需要CAN控制器,由于
Qsys没有CAN控制器IP核,而CAN控制器设计比较复杂,所以这里我们使用OpenCore网站上的
一个CAN Protocol Controller,网址为;该CAN控制
器具有如下特点:
✓ 非破坏性逐位仲裁(CSMA/CA)
✓ 基于消息的寻址/过滤
✓ 广播通讯
✓ 支持 1 Mbit/Sec 的操作
✓ 提供了 WISHBONE 总线接口和 8051 接口
✓ 兼容 SJA1000 接口。
下面我们用该CAN控制器来实现我们的CAN环回实验。
对于该CAN控制器,最大的方便之处是其兼容SJA1000接口,所以我们可以像使用SJA1000
那样来使用该CAN控制器。那SJA1000是什么呢?
SJA1000是一种独立CAN控制器,用于移动目标和一般工业环境中的区域网络控制
(CAN)。它是PHILIPS半导体PCA82C200 CAN控制器BasicCAN的替代产品,而且增加了一种新
的作模式PeliCAN,这种模式支持具有很多新特性的CAN 2.0B协议。对于SJA1000 CAN控制器
的具体说明可以参考我们提供的硬件资料的《CAN控制器SJA1000中文资料.pdf》文档。这里
就不多做介绍了。
另外对于该CAN控制器内部的代码由于后面我们直接封装成Qsys IP核使用,而且其提供
了SJA1000的接口,不需要了解其内部的代码逻辑,所以这里我们就不做介绍了。
为了方便使用,我们首先将该CAN控制器封装成Qsys IP核。由于该CAN控制器提供了
WISHBONE总线接口和8051接口,这里我们选用WISHBONE总线接口,使其与Avalon MM总线桥
接。下面我们介绍下如何封装成Qsys IP核。
首先我们在par目录下新建一个my_ip文件夹,接着在my_ip文件夹下新建一个can文件
夹,该文件夹用来放我们定制的CAN IP核文件。如同《自定义IP核—数码管》实验一样,我
们在can文件新建HAL、HDL、inc文件夹,如下图所示:
图 20.3.1 文件夹结构
现在我们将下载下来的(下载需注册OpenCore账号,可以直接用软件资料下的
can_la压缩包文件)CAN控制器压缩文件的trunk/rtl/verilog目录下的CAN控制
器Verilog HDL文件解压到刚才新建的HDL文件夹下,为了使用WISHBONE接口和使用Altera
RAM,我们对其中的can_de文件做如下修改:
图 20.3.2 修改宏定义
即将该两行取消注释,然后我们在该文件夹(HDL)下新建一个can_con文件,
内容如下:
1 module can_controller(
2 //module clock
3 input csi_clk ,
4 input rsi_reset,
5
6 //Avalon MM interface
7 input [7:0] avs_address,
8 input avs_chipselect,
9 input avs_write,
10 input avs_read,
11 input [7:0] avs_writedata,
12 output [7:0] avs_readdata,
13 output avs_waitrequest_n,
14
15 // CAN interface
16 input can_clk, //CAN控制器的操作时钟
17 input can_reset, //CAN控制器复位信号
18 input can_rx, //CAN控制器接收数据引脚
19 output can_tx, //CAN控制器发送数据引脚
20 output can_irq_n, //CAN控制器中断信号
21 output can_clkout //CAN控制器分频时钟输出信号
22
23 );
24
25 //*****************************************************
26 //** main code
27 //*****************************************************
28
29 can_top u1_can_top(
30 //Wishbone interface
31 .wb_clk_i (csi_clk ),
32 .wb_rst_i (rsi_reset | can_reset),
33 .wb_dat_i (avs_writedata[7:0] ),
34 .wb_dat_o (avs_readdata[7:0] ),
35 .wb_cyc_i (avs_write | avs_read ),
36 .wb_stb_i (avs_chipselect ),
37 .wb_we_i (avs_write & ~avs_read),
38 .wb_adr_i (avs_address[7:0] ),
39 .wb_ack_o (avs_waitrequest_n ),
40 //CAN interface
41 .clk_i (can_clk ),
42 .rx_i (can_rx ),
43 .tx_o (can_tx ),
44 .bus_off_on (),
45 .irq_on (can_irq_n ),
46 .clkout_o (can_clkout )
47 );
48
49 endmodule
50
该文件的功能是实现WISHBONE总线接口与Avalon总线接口的桥接。桥接方式可参考下
表:
图 20.3.3 Wishbone => Avalon
图 20.3.4 Avalon => Wishbone
另外对于CAN接口,我们将其封装成SJA1000的接口,clk_i是CAN控制器输入时钟引脚;
rx_i是CAN收发器传给CAN控制器的数据引脚;tx_o是CAN控制器传给CAN收发器的数据引脚;
irq_on中断输出引脚,用于向微控制器传递中断,低电平有效;clkout_o是SJA1000产生时钟
输出信号;bus_off_on没有对应的SJA1000引脚,所以这里就不使用该引脚。
接下来就是将can_con作为顶层文件将整个CAN控制器封装成Qsys IP核,这一步可参照《自定义IP核—数码管》实验。需要注意的是生成的can_con默认是
在par目录下,我们将其移动到can目录下时,需要更改PATH路径,如下图所示:
图 20.3.5 更改路径
当然了如果保持默认的存放位置的话是不用修改的,不过这样以后使用就比较麻烦。
封装成Qsys IP核之后,我们就可以在Qsys中调用了,如同调用PIO IP核一样简单。不
过,现在有一个问题,就是如何配置该CAN控制器IP核呢?
由于该CAN控制器兼容SJA1000 CAN控制器,所以我们可以按照配置SJA1000的方式来配
置。
为了方便以后的使用,下面我们就给该IP核添加寄存器头文件和底层驱动文件。我们将
SJA1000的寄存器列在文件里,该文件放在can/inc目录下。由于内容太长,
我们摘取部分如下图所示:
图 20.3.6 SJA1000寄存器文件
该文件中我们将SJA1000的两种CAN模式——BasicCAN模式和PeliCAN模式的寄存器都列出
来了。默认使用PeliCAN模式,该模式完全支持BasicCAN模式的功能,不过做的更好。另外对
于上图中的“命令寄存器及其位定义(写)”的“写”是表明该寄存器只能写或者读出来的
值无意义,后面的可参照该方式。
为了读者更好的使用SJA1000,我们重点介绍Peli模式下总线定时器和时钟分频寄存器。
总线定时寄存器分为总线定时寄存器0和总线定时寄存器1。
总线定时寄存器0定义了波特率预设值BRP和同步跳转宽度SJW的值。复位模式有效时这个
寄存器是可以被访问(读/写)的。如果选择的是PeliCAN模式,此寄存器在工作模式中是只
读的。在BasicCAN模式中总是FFH。位说明如下:
图 20.3.7 位说明
其中波特率预设值(BRP)决定了CAN系统时钟tSCL,以输入时钟can_clk(tCLK)为基准,
而且决定了相应的位时序。CAN系统时钟tSCL由如下公式计算:
tSCL = 2×tCLK×(32×BRP.5+16×BRP.4+8×BRP.3+4×BRP.2+2×BRP.1+BRP.0+1)
同步跳转宽度(SJW)定义了每一位周期可以被重新同步缩短或延长的时钟周期的最大数
目,是为了补偿在不同总线控制器的时钟振荡器之间的相位偏移,任何总线控制器必须在当
前传送的相关信号边沿重新同步,与CAN系统时钟tSCL的关系如下:
tSJW = tSCL×(2×SJW.1+SJW.0+1)
总线定时寄存器1定义了每个位周期的长度、采样点的位置和在每个采样点的采样数目。
在复位模式中,这个寄存器可以被读/写访问。在PeliCAN模式的工作模式中,这个寄存器是
只读的,在BasicCAN模式中总是FFH。位说明如下:
图 20.3.8 位说明
SAM位决定了是三倍采样(总线采样三次)还是单倍采样(总线采样一次),建议在高速
总线上使用单倍采样,此时SAM值为0。
时间段1(TSEG1)和时间段2(TSEG2)决定了每一位的时钟数目和采样点的位置,这里
tSYNCSEG = 1×tSCL
tTSEG1 = tSC×(8×TSEG1.3+4×TSEG1.2+2×TSEG1.1+TSEG1.0+1)
tTSEG2 = tSCL×(4×TSEG2.2+2×TSEG2.1+TSEG2.1+1)
图 20.3.9 总线定时器的设置与位时钟关系
时钟分频寄存器(CDR)为微控制器控制CLKOUT的频率以及屏蔽CLKOUT引脚输出,而且决
定了SJA1000是运行于BasicCAN模式还是PeliCAN模式。软件复位(复位请求/复位模式)时,
此寄存器不受影响。位说明如下:
图 20.3.10 位说明
保留位CDR.4总是0。应用软件总是向此位写0以与将来可能使用此位的特性兼容。对于该
寄存器我们一般使用的是BIT.7和BIT.3,即选择PeliCAN模式和关闭时钟输出。
另外对于SJA1000还有两类重要的寄存器:验收代码寄存器(ACR)和验收屏蔽寄存器AMR,
使用起来不难,只需要记住验收代码寄存器设置了CAN控制器接收怎样的ID和数据信息,而验
收屏蔽寄存器决定了验收代码寄存器的相应位起不起作用,当验收屏蔽寄存器的某一位为1
时,验收代码寄存器的相应位不起作用。
为了方便使用CAN控制器,我们实现了一些基本的底层驱动功能,驱动头文件
can_con放在can/HAL/inc目录下,内容如下:
1 #ifndef __CAN_CONTROLLER_H__
2 #define __CAN_CONTROLLER_H__
3
4 #include ";
5 #include <;
6 #include <io.h>
7 #include "al;
8 #include ""
9
10 #ifdef __cplusplus
11 extern "C"
12 {
13 #endif /* __cplusplus */
14
15 //复位设置结构体
16 typedef struct{
17 uint8_t btr0; //总线定时器BTR0
18 uint8_t btr1; //总线定时器BTR1
19 uint8_t cdr; //时钟分频寄存器CDR
20 uint8_t acr[4]; //验收代码寄存器ACR
21 uint8_t amr[4]; //验收屏蔽寄存器AMR
22 }_rest_val;
23
24 /* CAN_CONTROLLER function */
25 void can_brt0(uint8_t val);
26 void can_brt1(uint8_t val);
27 void can_ocr(uint8_t val);
28 void can_acp(const uint8_t *acceptance);
29 void can_rest(void);
30 void can_txf(uint8_t tx_data[], uint8_t tx_info);
31 void can_rxf();
32 void tx_id_f(uint8_t ff, const uint8_t *id);
33 void rx_id_f(uint8_t *id);
34 void rx_data_f(uint8_t *data);
35
36 /* Macros used by alt_sys_init */
37 #define CAN_CONTROLLER_INSTANCE(name, dev) alt_u32 canbaseaddr = name##_BASE
38 #define CAN_CONTROLLER_INIT(name, dev)
39 #ifdef __cplusplus
40 }
41 #endif /* __cplusplus */
42
43 #endif /* __SEGLED_CONTROLLER_H__ */
在代码第16行,我们定义了一个结构体,里面的值是我们使用CAN控制器之前需要设置的
值。我们还声明了一些函数,这些函数实现的功能可以查看can_con文件,主要是
为了方便使用CAN控制器。can_con文件代码如下:
1 #include <;
2 #include "can_con"
3
4 #define SIZE 13
5
6 extern alt_u32 canbaseaddr;
7
8 uint8_t tx_buffer[SIZE];
9 uint8_t rx_buffer[SIZE];
10
11 //默认的复位值
12 _rest_val rest_val={
13 0x00, //sjw=1tscl, tscl=2tclk
14 0x14, //位长时间为8tscl
15 CANMODE_BIT|CLKOFF_BIT,
16 {0x00,0x00,0x00,0x00},
17 {0xff,0xff,0xff,0xff}
18 };
19
20 /*****************************************************************
21 函数功能:写CAN ID
22 入口参数:ff:帧格式:1扩展帧,0标准帧:id:CAN ID
23 返回参数:
24 说明 :默认在Peli_CAN模式下
25 ******************************************************************/
26 void tx_id_f(uint8_t ff, const uint8_t *id)
27 {
28 if(ff){
29 tx_buffer[1] = id[1];
30 tx_buffer[2] = id[2];
31 tx_buffer[3] = id[3];
32 tx_buffer[4] = id[4];
33 }
34 else{
35 tx_buffer[1] = id[1];
36 tx_buffer[2] = id[2];
37 }
38 }
39
40 /*****************************************************************
41 函数功能:读CAN ID
42 入口参数:id:接收到的CAN ID,id[0]为帧信息
43 返回参数:
44 说明 :默认在Peli_CAN模式下
45 ******************************************************************/
46 void rx_id_f(uint8_t *id)
47 {
48 uint8_t i;
49 if(rx_buffer[0] & 0x80)
50 for(i=0; i < 5; i++)
51 id[i] = rx_buffer[i];
52 else
53 for(i=0; i < 3; i++)
54 id[i] = rx_buffer[i];
55 }
56
57 /*****************************************************************
58 函数功能:读取接收到的数据
59 入口参数:data:接收数据的数组
60 返回参数:
61 说明 :默认在Peli_CAN模式下
62 ******************************************************************/
63 void rx_data_f(uint8_t *data)
64 {
65 uint8_t i;
66 if(rx_buffer[0] & 0x80)
67 for(i=0; i < (rx_buffer[0] & 0x0f); i++)
68 data[i] = rx_buffer[i+5];
69 else
70 for(i=0; i < (rx_buffer[0] & 0x0f); i++)
71 data[i] = rx_buffer[i+3];
72 }
73
74 /*****************************************************************
75 函数功能:设置总线定时器0
76 入口参数:val:总线定时器0的值
77 返回参数:
78 说明 :默认在Peli_CAN模式下
79 ******************************************************************/
80 void can_brt0(uint8_t val)
81 {
82 re = val;
83
84 }
85
86 /*****************************************************************
87 函数功能:设置总线定时器1
88 入口参数:val:总线定时器1的值
89 返回参数:
90 说明 :默认在Peli_CAN模式下
91 ******************************************************************/
92 void can_brt1(uint8_t val)
93 {
94 re = val;
95 }
96
97 /*****************************************************************
98 函数功能:设置输出控制寄存器
99 入口参数:val:输出控制寄存器的值
100 返回参数:
101 说明 :默认在Peli_CAN模式下
102 ******************************************************************/
103 void can_ocr(uint8_t val)
104 {
105 re = val;
106 }
107
108 /*****************************************************************
109 函数功能:设置验收滤波器
110 入口参数:acceptance:验收滤波器设置
111 返回参数:
112 说明 :默认在Peli_CAN模式下
113 ******************************************************************/
114 void can_acp(const uint8_t *acceptance)
115 {
116 re[0] = acceptance[0]; //验收代码寄存器设置
117 re[1] = acceptance[1];
118 re[2] = acceptance[2];
119 re[3] = acceptance[3];
120 re[0] = acceptance[4]; //验收屏蔽寄存器设置
121 re[1] = acceptance[5];
122 re[2] = acceptance[6];
123 re[3] = acceptance[7];
124 }
125
126 /*****************************************************************
127 函数功能:复位SJA1000
128 入口参数:
129 返回参数:
130 说明 :默认在Peli_CAN模式下
131 ******************************************************************/
132 void can_rest(void)
133 {
134 //确定是否在复位模式,不在则进入复位模式
135 while(!(IORD_8DIRECT(canbaseaddr, SJA_MOD) & RM_BIT))
136 IOWR_8DIRECT(canbaseaddr, SJA_MOD, RM_BIT);
137
138 IOWR_8DIRECT(canbaseaddr, SJA_BTR0, re);
139 IOWR_8DIRECT(canbaseaddr, SJA_BTR1, re); //位长时间为8tscl
140 //设为Peli_CAN模式&禁能时钟输出
141 IOWR_8DIRECT(canbaseaddr, SJA_CDR, re);
142
143 IOWR_8DIRECT(canbaseaddr, SJA_ACR0, re[0]); //验收代码寄存器设置
144 IOWR_8DIRECT(canbaseaddr, SJA_ACR1, re[1]);
145 IOWR_8DIRECT(canbaseaddr, SJA_ACR2, re[2]);
146 IOWR_8DIRECT(canbaseaddr, SJA_ACR3, re[3]);
147 IOWR_8DIRECT(canbaseaddr, SJA_AMR0, re[0]); //验收屏蔽寄存器设置
148 IOWR_8DIRECT(canbaseaddr, SJA_AMR1, re[1]);
149 IOWR_8DIRECT(canbaseaddr, SJA_AMR2, re[2]);
150 IOWR_8DIRECT(canbaseaddr, SJA_AMR3, re[3]);
151
152 IOWR_8DIRECT(canbaseaddr, SJA_IER, RIE_BIT); //使能接收中断
153 IOWR_8DIRECT(canbaseaddr, SJA_CMR, RRB_BIT); //释放接收缓冲器
154
155 IOWR_8DIRECT(canbaseaddr, SJA_MOD, AFM_BIT); //设置为单个验收滤波器
156 while(!(IORD_8DIRECT(canbaseaddr, SJA_MOD) == AFM_BIT))
157 IOWR_8DIRECT(canbaseaddr, SJA_MOD, AFM_BIT);
158 printf("INIT_DONE!n");
159 }
160
161 /*****************************************************************
162 函数功能:CAN控制器发送函数
163 入口参数:tx_data[]:CAN控制器发送的数据
164 tx_info:发送帧信息,bit7:0 标准帧,1扩展帧,
165 bit6:0:数据帧,1遥控帧
166 bit3-0:数据长度DLC
167 返回参数:
168 说明 :发送帧信息
169 ******************************************************************/
170 void can_txf(uint8_t tx_data[], uint8_t tx_info)
171 {
172 uint8_t i;
173
174 if(tx_info & 0x80){ //判断是否为扩展帧
175 tx_buffer[0] = tx_info;
176 for(i=0; i < (tx_info & 0x0f); i++)
177 tx_buffer[5+i] = tx_data[i];
178 }
179 else{
180 tx_buffer[0] = tx_info;
181 for(i=0; i < (tx_info & 0x0f); i++)
182 tx_buffer[3+i] = tx_data[i];
183 }
184 while(IORD_8DIRECT(canbaseaddr, SJA_SR) & RS_BIT); //等待
185 while(!(IORD_8DIRECT(canbaseaddr, SJA_SR) & (TCS_BIT|TBS_BIT))); //等待
186 IOWR_8DIRECT(canbaseaddr, SJA_TBR0 , tx_buffer[ 0]);
187 IOWR_8DIRECT(canbaseaddr, SJA_TBR1 , tx_buffer[ 1]);
188 IOWR_8DIRECT(canbaseaddr, SJA_TBR2 , tx_buffer[ 2]);
189 IOWR_8DIRECT(canbaseaddr, SJA_TBR3 , tx_buffer[ 3]);
190 IOWR_8DIRECT(canbaseaddr, SJA_TBR4 , tx_buffer[ 4]);
191 IOWR_8DIRECT(canbaseaddr, SJA_TBR5 , tx_buffer[ 5]);
192 IOWR_8DIRECT(canbaseaddr, SJA_TBR6 , tx_buffer[ 6]);
193 IOWR_8DIRECT(canbaseaddr, SJA_TBR7 , tx_buffer[ 7]);
194 IOWR_8DIRECT(canbaseaddr, SJA_TBR8 , tx_buffer[ 8]);
195 IOWR_8DIRECT(canbaseaddr, SJA_TBR9 , tx_buffer[ 9]);
196 IOWR_8DIRECT(canbaseaddr, SJA_TBR10, tx_buffer[10]);
197 IOWR_8DIRECT(canbaseaddr, SJA_TBR11, tx_buffer[11]);
198 IOWR_8DIRECT(canbaseaddr, SJA_TBR12, tx_buffer[12]);
199
200 IOWR_8DIRECT(canbaseaddr, SJA_CMR, TR_BIT); //置位发送请求位
201 printf("TX DONE!n");
202 }
203
204 /*****************************************************************
205 函数功能:接收节点发送的帧信息
206 入口参数:
207 返回参数:
208 说明 :在中断服务程序中调用
209 ******************************************************************/
210 void can_rxf()
211 {
212 rx_buffer[ 0] = IORD_8DIRECT(canbaseaddr, SJA_RBR0 );
213 rx_buffer[ 1] = IORD_8DIRECT(canbaseaddr, SJA_RBR1 );
214 rx_buffer[ 2] = IORD_8DIRECT(canbaseaddr, SJA_RBR2 );
215 rx_buffer[ 3] = IORD_8DIRECT(canbaseaddr, SJA_RBR3 );
216 rx_buffer[ 4] = IORD_8DIRECT(canbaseaddr, SJA_RBR4 );
217 rx_buffer[ 5] = IORD_8DIRECT(canbaseaddr, SJA_RBR5 );
218 rx_buffer[ 6] = IORD_8DIRECT(canbaseaddr, SJA_RBR6 );
219 rx_buffer[ 7] = IORD_8DIRECT(canbaseaddr, SJA_RBR7 );
220 rx_buffer[ 8] = IORD_8DIRECT(canbaseaddr, SJA_RBR8 );
221 rx_buffer[ 9] = IORD_8DIRECT(canbaseaddr, SJA_RBR9 );
222 rx_buffer[10] = IORD_8DIRECT(canbaseaddr, SJA_RBR10);
223 rx_buffer[11] = IORD_8DIRECT(canbaseaddr, SJA_RBR11);
224 rx_buffer[12] = IORD_8DIRECT(canbaseaddr, SJA_RBR12);
225
226 IOWR_8DIRECT(canbaseaddr, SJA_CMR, RRB_BIT); //释放接收缓冲器
227
228 printf("RX DONE!n");
229 }
需要注意的是这些函数都是默认在PeliCAN模式下工作的,而且与SJA1000不同的是我们默认的就是PeliCAN模式。我们在代码第12行设置了复位时的默认值,如果需要重新设置只需
调用相应的函数即可。另外这些函数包括了初始化SJA1000,发送CAN信息,接收CAN信息等基
本功能,如果想实现更复杂的功能,譬如错误处理,可以在该文件中添加错误处理函数即
可。
编写完了寄存器头文件和驱动底层头文件,为了让Nios SBT for Eclipse自动获取IP核
的HAL,我们需要编写can_con文件,由于该代码比较简单,并且该代码中的
内容与我们的自定义数据管IP核实验中的文件中的代码基本一致,所以我们就不再对
can_con进一步进行讲解了。现在为了方便以后我们在其它工程中调用CAN控
制器IP核,我们将my_ip文件夹拷贝到<Quartus安装目录>/ip文件夹下,至此封装IP核结束。
现在我们实现Qsys的CAN环回实验。
搭建的Qsys硬件框架如下:
图 20.3.11 硬件框架
这里我们除了基本的sdram和epcs外,增加了我们数码管、按键控制发送的PIO和刚才封
装的CAN控制器。CAN控制器的时钟can_clk_i我们引出来,接到顶层的PLL的输出的16MHz上,
can_clk_o是CAN控制器的输出时钟,一般不需要。
顶层代码如下:
1 module qsys_can_loopback(
2 //module clock
3 input sys_clk , //系统时钟,50Mhz
4 input sys_rst_n , //系统复位,低电平有效
5
6 //SDRAM interface
7 output sdram_clk , //SDRAM 芯片时钟
8 output sdram_cke , //SDRAM 时钟有效
9 output sdram_cs_n , //SDRAM 片选
10 output sdram_ras_n, //SDRAM 行有效
11 output sdram_cas_n, //SDRAM 列有效
12 output sdram_we_n , //SDRAM 写有效
13 output [ 1:0] sdram_ba , //SDRAM Bank地址
14 output [12:0] sdram_addr , //SDRAM 行/列地址
15 inout [15:0] sdram_data , //SDRAM 数据
16 output [ 1:0] sdram_dqm , //SDRAM 数据掩码
17
18 //EPCS FLASH interface
19 output epcs_dclk , // EPCS 时钟信号
20 output epcs_sce , // EPCS 片选信号
21 output epcs_sdo , // EPCS 数据输出信号
22 input epcs_data0 , // EPCS 数据输入信号
23
24 //can interface
25 input can_rx , // CAN接收引脚
26 output can_tx , // CAN发送引脚
27
28 //segled interface
29 output [ 5:0] sel , // 数码管位选端
30 output [ 7:0] seg_led , // 数码管段选端
31
32 //key interface
33 input key2 // 使能CAN发送信号
34 );
35
36 //wire define
37 wire clk_100m; //SDRAM 控制器时钟
38 wire can_clk ; //CAN驱动器的时钟
39 wire rst_n ; //系统复位信号
40 wire locked ; //PLL输出稳定标志
41
42 //*****************************************************
43 //** main code
44 //*****************************************************
45
46 //待PLL输出稳定之后,停止系统复位
47 assign rst_n = sys_rst_n & locked;
48
49 //例化PLL
50 pll_clk u_pll_clk(
51 .areset (~sys_rst_n),
52 .inclk0 (sys_clk ),
53 .c0 (clk_100m ),
54 .c1 (sdram_clk ),
55 .c2 (can_clk ), // CAN控制器驱动时钟16MHz
56 .locked (locked )
57 );
58
59 //例化Nios2系统模块
60 nios2os u_nios2os (
61 .clk_clk (clk_100m ), // 时钟100M
62 .reset_reset_n (rst_n ), // 复位信号
63 .sdram_addr (sdram_addr ), // SDRAM 行/列地址
64 .sdram_ba (sdram_ba ), // SDRAM Bank地址
65 .sdram_cas_n (sdram_cas_n), // SDRAM 列有效
66 .sdram_cke (sdram_cke ), // SDRAM 时钟有效
67 .sdram_cs_n (sdram_cs_n ), // SDRAM 片选
68 .sdram_dq (sdram_data ), // SDRAM 数据
69 .sdram_dqm (sdram_dqm ), // SDRAM 数据掩码
70 .sdram_ras_n (sdram_ras_n), // SDRAM 行有效
71 .sdram_we_n (sdram_we_n ), // SDRAM 写有效
72 .epcs_dclk (epcs_dclk ), // EPCS 时钟信号
73 .epcs_sce (epcs_sce ), // EPCS 片选信号
74 .epcs_sdo (epcs_sdo ), // EPCS 数据输出信号
75 .epcs_data0 (epcs_data0 ), // EPCS 数据输入信号
76 .can_clk_i_clk (can_clk ), // can_clk_in
77 .can_rt_rx (can_rx ), // can_rt.rx
78 .can_rt_tx (can_tx ), // .tx
79 .can_clk_out_clk ( ), // can_clk_out
80 .can_tx_en_export (key2 ), // 使能CAN发送信号
81 .segled_sel (sel ), // 数码管位选端
82 .segled_seg_led (seg_led ) // 数码管段选端
83 );
84
85 endmodule
顶层代码主要是连接例化模块的各端口,实现信号的交互。
接下来进行引脚分配,常用的引脚我们就不贴图了,我们贴下CAN控制器使用到的引脚和
按键使能CAN发送的引脚,如下表所示:
图 20.3.12 CAN环回实验部分管脚分配
软件设计
创建好软件工程后,我们将更改为q。接下来我们来
看一下本实验的软件工程代码,如下:
1 #include ";
2 #include <uni;
3 #include <;
4 #include <;
5 #include "sy;
6 #include "sy;
7 #include "al; //PIO寄存器文件
8 #include "can_con" //CAN控制器驱动文件
9 #include "segled_con" //数码管驱动文件
10
11 #define CanBaseAddr CAN_CONTROLLER_BASE
12 //帧信息
13 #define FF 0 //帧格式: 0:SFF 1:EFF
14 #define RTR 0 //0数据帧 1远程帧
15 #define DLC 3 //数据长度(0~8)
16 #define FRM_INFO (FF<<7) + (RTR<<6) + DLC //帧信息
17
18 const uint8_t id[2]={0x00,0x20}; //发送的ID
19
20 uint8_t acceptance[8]={0x00,0x00,0x00,0x00, //验收代码
21 0xff,0xff,0xff,0xff}; //验收屏蔽
22 uint8_t rx_data[8]; //接收的数据
23 uint8_t rx_flag = 0; //接收有效标志
24
25 void can_isr(void* context, alt_u32 id);
26 void delay_ms(uint32_t n);
27
28 int main(void)
29 {
30 int rc;
31 uint8_t tx_data[8],i;
32 uint8_t key;
33
34 printf("Hello from Nios II!n");
35 //需发送数据
36 tx_data[0] = 18;
37 tx_data[1] = 12;
38 tx_data[2] = 25;
39 IOWR_AVALON_SEGLED_EN(SEGLED_CONTROLLER_BASE,1);
40 alt_ic_isr_register(
41 CAN_CONTROLLER_IRQ_INTERRUPT_CONTROLLER_ID,
42 CAN_CONTROLLER_IRQ,
43 can_isr,
44 0,
45 0); //注册CAN控制器中断服务
46 can_brt0(0x00); //设置总线定时器0 sjw=1tscl, tscl=2tclk
47 can_brt1(0x14); //设置总线定时器1 位长时间为8tscl
48 can_acp(acceptance); //设置验收滤波器
49 can_rest(); //复位CAN控制器
50 while(1){
51 key = IORD_ALTERA_AVALON_PIO_DATA(CAN_TX_EN_BASE);
52 if(!key){
53 tx_id_f(FF,id); //发送ID
54 can_txf(tx_data,FRM_INFO); //发送数据
55 delay_ms(500);
56 }
57 else if(rx_flag){
58 rx_data_f(rx_data);
59 for(i=0;i<DLC;i++){
60 IOWR_AVALON_SEGLED_DATA(SEGLED_CONTROLLER_BASE,rx_data[i]);
61 delay_ms(1000);
62 }
63 rx_flag = 0;
64 }
65 }
66 return 0;
67 }
68
69 /*****************************************************************
70 函数功能:延时函数
71 入口参数:n:延时的时间
72 返回参数:
73 说明 :单位ms
74 ******************************************************************/
75 void delay_ms(uint32_t n)
76 {
77 usleep(n*1000);
78 }
79
80 /*****************************************************************
81 函数功能:中断服务函数
82 入口参数:
83 返回参数:
84 说明 :CAN 控制器的中断服务函数
85 ******************************************************************/
86 void can_isr(void* context, alt_u32 id)
87 {
88 uint8_t status;
89 status = IORD_8DIRECT(CanBaseAddr, SJA_IR);
90 printf("status:%xn",status);
91 if(status & RI_BIT){ //判断是否是接收中断
92 can_rxf(); //接收数据
93 rx_flag = 1;
94 }
95 }
代码第13行起,我们宏定义了帧格式、数据长度等信息。另外第20行的验收屏蔽位设为全1,这样就可以接收所有的ID信息。代码第36行起的3行我们定义了需发送的数据18、12、
15,发送3字节的数据与代码第15行的DLC对应。代码第46和47行的总线定时寄存器配置后,
在16MHz的CAN控制器输入时钟下,CAN控制器输出的波特率为1Mbps。代码第49行起CAN控制器
复位之后处于接收状态,当接收到有效数据后触发接收中断,将接收到的数据显示在数码管
上,如果是按下按键,就发送数据。
下载验证
讲完了软件工程,接下来我们就将该实验下载至我们的开拓者开发板进行验证。
首先我们将两个开拓者开发板上的CAN接口用两根杜邦线连接起来,如图 20.5.1所示。
连接时注意接口位置一一对应,不要接反了。另外开拓者开发板上的CAN接口与RS485接口十
分相像,使用时请注意区分。还有一点需要注意的是,两块开发板的P5口都需要使用杜邦线
或者跳帽进行连接选择CAN口,否则无法进行CAN通信。然后分别将两个开发板上的下载器一
端连电脑,另一端与开发板上的JTAG下载端口连接,最后连接电源线并打开电源开关。然后
我们将程序固化到开发板上。
图 20.5.1 连接及结果图
固化完成后我们依次按下任意一个开发板上KEY2按键,可以观察到另外一个开发板上依次显示收到的数据18、12和25,说明程序下载验证成功。
1.文章《【奥迪tts保养怎么复位】定时原子先驱NiosII资料连载第20章CAN通信实验》援引自互联网,为网友投稿收集整理,仅供学习和研究使用,内容仅代表作者本人观点,与本网站无关,侵删请点击页脚联系方式。
2.文章《【奥迪tts保养怎么复位】定时原子先驱NiosII资料连载第20章CAN通信实验》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
相关推荐
- . 现代买票为什么带上携程保险
- . 潮阳怎么去广州南站
- . 湖南马拉河怎么样
- . 烧纸为什么到三岔路口
- . 百色为什么这么热
- . 神州租车怎么样
- . 芜湖方特哪个适合儿童
- . 护肤品保养液是什么类目
- . 早晚的护肤保养有哪些项目
- . 女孩护肤品怎么保养的最好