CAN-bus现场总线基础教程【第3章】CAN控制器驱动-SJA1000编程基础(9)

合集下载
相关主题
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

第3章 CAN 控制器驱动
1.1 SJA1000编程基础
1.1.1 MCU 访问SJA1000
SJA1000使用并行总线接口与MCU 连接,对MCU 来说,SJA1000可以认为是1个外扩的RAM 芯片,51系列MCU 通过地址线、数据线和控制线与SJA1000连接,如图3.1所示。

AD[0:7]是低8位地址与数据总线复用的,MCU 在操作总线时,在该接口上先输出低8位地址线,然后再进行数据操作(读或写)。

SJA1000内部带有地址锁存器,由ALE 信号实现数据与地址的分离。

因为SJA1000的地址宽度为8位,所以寻址空间范围是0x00~0xFF 。

假如每个地址都对应一个寄存器,那么SJA1000最多支持256个寄存器。

而实际上SJA1000在BasicCAN (CAN2.0A )模式下只有32个寄存器,在FullCAN (CAN2.0B )模式下则有128个寄存器。

虽然SJA1000寄存器的访问地址会因为硬件设计不同而不同,但SJA1000内部寄存器的位置关系是固定的。

如果我们给SJA1000每个内部寄存器的地址都定义绝对地址(如程序清单3.1所示),那么在硬件设计发生变化时,特别是器件编址变化时,要修改的寄存器地址定义将会非常多。

为了提高驱动的可移植性,在实际访问SJA1000内部寄存器时,常采用基地址加偏移量的方式进行寄存器访问(如程序清单3.2所示)。

如果把SJA1000内部寄存器看做数组的话,那基地址就是这个数组的首地址,偏移量就是数组的下标,即成员在数组中的位置。

程序清单3.1 采用绝对编址的寄存器定义
1 #define REG_CAN_MOD 0xA000 // 内部控制寄存器
2 #define REG_CAN_CMR 0xA001 // 命令寄存器
3 #define REG_CAN_SR 0xA002 // 状态寄存器
4 #define REG_CAN_IR 0xA003 // 中断寄存器
5 #define REG_CAN_IER
0xA004
// 中断使能寄存器
6
......
程序清单3.2 采用基地址加偏移量方式的寄存器定义
7 #define REG_BASE_ADD
0xA000
// SJA1000寄存器基地址 8 #define
REG_CAN_MOD 0x00 // 内部控制寄存器 9 #define REG_CAN_CMR 0x01 // 命令寄存器 10 #define REG_CAN_SR 0x02 // 状态寄存器 11 #define REG_CAN_IR 0x03 // 中断寄存器 12 #define REG_CAN_IER
0x04
// 中断使能寄存器
13
......
通常MCU 的总线上会挂载很多器件,除了SJA1000外,可能还有RAM 和ROM 等器件。

所以数据总线和地址总线上的信息并不一定都是给SJA1000的。

因此当MCU 在访问SJA1000
图3.1 SJA1000与MCU 扩展总线连接
时,为了明确的表明下面的操作对象是SJA1000,MCU 会把SJA1000的片选(CS )信号拉低。

SJA1000的片选信号由MCU 未使用的高位地址线经过译码电路产生,译码电路的设计决定了MCU 访问SJA1000的基地址。

MCU 产生片选信号有以下三种方法。

1. 线选法
所谓线选法,就是直接利用MCU 的空闲高位地址线作为扩展芯片的片选信号。

优点是线路连接简单明了,无需另外增加译码电路,缺点是寻址范围不唯一(可通过多个地址访问扩展芯片),地址空间没有被充分利用,使用单个扩展芯片时可使用该方法。

2. 全地址译码法
全地址译码法利用译码器对未使用的高位地址线进行译码,使用译码器的输出作为扩展芯片的片选信号,常用的译码芯片有74HC138、74HC139、74HC154等。

优点是扩展器件的每个存储单元具有唯一的访问地址,不存在地址重叠现象,对地址的空间的使用是连续的,能有效地利用地址空间,缺点是所需译码电路较多,外扩芯片较多时使用该方法。

3. 部分地址译码法
部分地址译码法对未使用的部分高位地址线进行译码。

优点是使用译码电路较少,缺点是外扩器件地址范围有重叠。

在本设计中,通过并行总线扩展的器件只有SJA1000,可以采用线选法产生SJA1000的片选信号,但考虑到系统的扩展性,我们使用部分地址译码法来产生SJA1000的片选信号。

如图3.2所示,SJA1000的片选信号通过对地址线A15~A12译码来得到。

74HC139内部包含了两个“2线—4线”译码器,其真值表如表3.1所示。

根据图3.2中74HC139的电路连接关系,对比74HC139的真值表,我们可以发现,当A14为低电平、A15为高电平时,1Y2输出为低电平;当A12为低电平、A13为高电平并且1Y2为低电平时,2Y2输出为低电平,即SJA1000片选信号有效,所以只要MCU 在访问SJA1000时16位地址线上输出地址为0b1010 xxxx
xxxx xxxx ,SJA1000的片选信号就会有效。

因此SJA1000的访问基址为0xA000~0xAF00,为了编写程序方便,在以后的例程中,MCU 访问SJA1000的基地址统一使用0xA000。

1.1.2 读写寄存器
在C 语言中,一般使用指针来访问存储空间,SJA1000按照存储器的方式与MCU 连接,所以我们可以通过指针的方式来访问SJA1000的寄存器,实现对SJA1000的操作控制。

使用C 语言读写SJA1000寄存器的代码如程序清单3.3所示。

图3.2 SJA1000片选译码电路
程序清单3.3 读写SJA1000寄存器
14#define SJA_BASE_ADDR 0xA000 // 定义SJA1000访问基址
15xdata unsigned char *SJA_CS_Point = ( xdata unsigned char *) SJA_BASE_ADDR;
16// 写SJA1000寄存器
17void WriteSJAReg (unsigned char RegAdr,unsigned char Value)
18{
19*(SJA_CS_Point + RegAdr) = Value;
20return ;
21}
22// 读SJA1000寄存器
23unsigned char ReadSJAReg (unsigned char RegAdr)
24{
25return (*(SJA_CS_Point + RegAdr));
26}
程序清单3.3第1行定义了访问SJA1000的基地址,按照电路的连接关系,该值为0xA0000。

第2行定义了一个指向外部存储空间的指针变量,并将其指向SJA1000访问的基地址,对
SJA1000寄存器的读写通过操作该指针来完成。

xdata是C51编译器用于指定存储类型的扩展关键字,表示用MOVX@DPTR指令访问的外部存储器空间。

( xdata unsigned char *)完成强制类型转换功能,将SJA_BASE_ADDR强制转换为1个xdata unsigned char类型的指针。

程序清单3.3第3~13行定义了对SJA1000寄存器的读写函数,在下文中对SJA1000的所有操作都以这2个函数为基础实现的。

1.1.3 寄存器位操作
在控制SJA1000时,有时我们需要对某些寄存器进行位操作(只改写指定位,其它位保持不变),如进入复位模式(模式寄存器的第0位置1)、退出复位模式(模式寄存器的第0位清0)。

对寄存器的位操作一般是通过“读、改、写”的方式实现,基本步骤如下:
●读寄存器的当前值;
●根据需要修改读到的值的指定位;
●将修改后的值写回寄存器。

基于以上原理,我们实现“设置寄存器位”和“清零寄存器位”函数,以方便对SJA1000的操作。

1.设置寄存器位
设置寄存器位函数用于将指定寄存器的指定位置1,具体实现见程序清单3.4。

程序清单3.4 设置寄存器位函数
27/****************************************************************************************** ** 函数名称:SetBitMask
28** 函数功能:设置指定寄存器的指定位为1
29** 输入参数:RegAdr->寄存器地址
30** BitValue ->设置的位值
31** 输出参数:无
32** 返回值:1=操作成功,0=操作失败
33******************************************************************************************/ 34char SetBitMask(unsigned char RegAdr,unsigned char BitValue)
35{
36char status=0;
37unsigned char temp;
38temp = ReadSJAReg(RegAdr); // 读取寄存器的当前值
39t emp = temp | BitValue; // 将寄存器值的BitValue指定的位置1
40WriteSJAReg(RegAdr,temp); // 重写寄存器
41if(ReadSJAReg(RegAdr) == temp){ // 判断寄存器是否写成功
42status = 1; // 设置状态值为1
43} else {
44status = 0 ; // 设置状态值为0
45}
46return (status); // 返回状态值
47}
2.清零寄存器位
清零寄存器位函数用于将指定寄存器的指定位置0,具体实现见程序清单3.5。

程序清单3.5 清零寄存器位函数
48/****************************************************************************************** ** 函数名称:ClearBitMask
49** 函数功能:将指定寄存器的指定位清0
50** 输入参数:RegAdr -> 寄存器地址
51** BitValue -> 设置的位值
52** 输出参数:无
53** 返回值:1=操作成功,0=操作失败
54******************************************************************************************/ 55char ClearBitMask(unsigned char RegAdr,unsigned char BitValue)
56{
57char status=0;
58unsigned char temp;
59temp = ReadSJAReg(RegAdr); // 读取寄存器的当前值
60temp = temp&(~BitValue); // 将寄存器值的BitValue指定的位清零
61WriteSJAReg(RegAdr,temp); // 重写寄存器
62if(ReadSJAReg(RegAdr) == temp){ // 判断寄存器是否写成功
63status = 1; // 设置状态参数为1
64} else {
65status = 0 ; // 设置状态参数为0
66}
67return (status); // 返回操作标志
68}
1.1.4 连续读写寄存器
在访问SJA1000时,我们需要连续读写多个寄存器,如设置验收滤波器、收发CAN报文等,为方便这类程序的编写,我们可以先实现连续读写寄存器函数。

1.连续写寄存器
连续写寄存器函数通过循环调用写寄存器函数实现,详见程序清单3.6。

程序清单3.6 连续写多个寄存器
69/****************************************************************************************** ** 函数名称:WriteSJARegBlock
70** 函数功能:连续写多个寄存器
71** 输入参数:RegAdr -> 寄存器起始地址
72** ValueBuf -> 写寄存器时使用的指针
73** len -> 要连续写入的寄存器数
74** 输出参数:无
75** 返回值:连续写入的寄存器数
76******************************************************************************************/ 77char WriteSJARegBlock(unsigned char RegAdr,unsigned char *ValueBuf, unsigned char len)
78{
79unsigned char i;
80if(len != 0){
81for(i=0;i<len;i++){
82WriteSJAReg (RegAdr+i,ValueBuf[i]);
83}
84}
85return len;
86}
2.连续读寄存器
连续读寄存器函数通过循环调用读寄存器函数实现,详见程序清单3.7。

程序清单3.7 连续读多个寄存器
87/****************************************************************************************** 88** 函数名称:ReadSJARegBlock
89** 函数功能:连续读多个寄存器
90** 输入参数:RegAdr -> 寄存器起始地址
91** ValueBuf -> 读寄存器时使用的指针
92** len -> 要连续读取的寄存器数
93** 输出参数:无
94** 返回值:连续读到的寄存器数
95******************************************************************************************/ 96char ReadSJARegBlock(unsigned char RegAdr,unsigned char *ValueBuf, unsigned char len)
97{
98unsigned char i;
99if(len != 0){
100for(i=0;i<len;i++){
101ValueBuf[i] = ReadSJAReg(RegAddr+i);
102}
103}
104return len;
105}
1.1.5 精确延时
延时是程序设计中经常使用的功能,常规的延时程序结构详见程序清单3.8。

这种结构的延时程序,在不同的MCU平台上,需要根据实际测试结果调整延时内循环的循环次数,使内循环的执行时间达到期望,使用不便,移植困难。

程序清单3.8 常规延时函数
106/****************************************************************************************** 107** 函数名称:Delay
108** 函数功能:软件延时
109** 输入参数:n –> 软件延时的单位时间数
110** 输出参数:无
111** 返回值:无
112******************************************************************************************/ 113void Delay(unsigned int n)
114{
115int i;
116do {
117for(i=0 ;i < 100 ;i++); // 延时内循环,延时1个固定的时间
118} while (--n != 0); // 循环n次
119}
针对常规延时函数的不足,程序清单3.9通过使用MCU定时器的定时功能实现了一个精确延时函数。

程序清单3.9 精确延时函数
120// 定时器0初始化函数
121void timerInit(void)
122{
123TMOD &= ~T0_MASK; // 清除旧设置(#define T0_MASK 0x0F)
124TMOD |= 0x01; // 设置新模式:16位定时模式
125}
126
127/****************************************************************************************** 128** 函数名称:timerDelay
129** 函数功能:通过使用定时器实现精确延时
130** 输入参数:n -> 延时的10ms个数
131** 输出参数:无
132** 返回参数:无
133******************************************************************************************/ 134void timerDelay(unsigned int n) // 延时(0.01 * n)秒
135{
136do{
137// Timer装载(定时10ms)
138TL0 = LOW_BYTE(65536UL - CPUCLK / 100);
139TH0 = HIGH_BYTE(65536UL - CPUCLK / 100);
140TR0 = 1; // 启动Timer
141while (!TF0); // 等待Timer溢出
142TR0 = 0; // 关闭Timer
143TF0 = 0; // 清除溢出标志
144}while (--n != 0); // 循环n次
145}
程序清单3.9的第1~5行的timerInit函数将MCU定时器0设置为16位定时工作模式。

程序清单3.9的第17~25行通过等待定时器0计数溢出的方式来实现1个10ms的延时内循环。

定时器0由timerInit函数初始化为定时工作方式,在该方式下,定时器0对MCU内部时钟12分频后的脉冲(在config.h中定义为CPUCLK)进行自加计数,当计数器初值设为“65536UL - CPUCLK / 100”时,计数器的计数值在启动后10ms时溢出,从而实现了10ms的精确延时。

相关文档
最新文档