CAN通讯的优点在此就不多说了,10公里,5Kb/s的速度是能保证的。
第一步:硬件环境的建立。
这里采用的是SJA1000作为总线控制器,CTM8251模块作为总线驱动器。MCU采用的是MEGA16:利用I/O口模拟数据总线,当然也可以使用有总线的MCU:MCS-51,MEGA8515等。
原理图如下:
第二步:SJA1000的控制
首先阅读下SJA1000的手册,基本了解下SJA1000的结构,主要是寄存器方面的。还要了解下CAN总线方面的东西:BasicCAN,PeliCAN,远程帧,数据帧等等……
SJA1000工作之前需要配置一下,才能正常工作,没有经过配置的SJA1000回拉坏总线的:组成网络的时候,如果其中有的SJA1000没有正确配置,这个设备会干扰总线,使其它设备的数据发送不出去。
怎么才能控制SJA1000呢,请看下面的SJA1000读写的时序图:
写的时序
根据时序要求,可以利用I/O口模拟总线了:
//**************************读SJA1000*************************//
uintRead_SJA1000(uintaddress)
{
uchardata;
asm("nop");
ALE_off;
WR_on;
RD_on;
CAN_cs_on;
DDRA=0xff;//数据口为输出
PORTA=address;//输出数据的地址
asm("nop");//delay5us(1);
ALE_on;
asm("nop");//delay5us(1);
//DDRA=0xff;//数据口为输出
PORTA=address;//输出数据的地址//再次输出地址,确保一致。
asm("nop");//delay5us(1);
ALE_off;
//delay5us(1);
CAN_cs_off;
RD_off;
asm("nop");//delay5us(2);
asm("nop");
DDRA=0x00;//数据口为输入
PORTA=0xff;//上拉
asm("nop");
data=PINA;//获得数据
asm("nop");//delay5us(1);
RD_on;
CAN_cs_on;
asm("nop");//delay5us(2);
//dog();
returndata;
}
//**************************写SJA10000*************************//
voidWrite_SJA1000(uintaddress,uintdata)
{asm("nop");
//uinttemp1,temp2;
DDRA=0xff;//数据口为输出
PORTA=address;//输出数据的地址
CAN_cs_on;
ALE_off;
WR_on;
RD_on;
asm("nop");//delay5us(1);
ALE_on;
asm("nop");//delay5us(1);
//DDRA=0xff;//数据口为输出
PORTA=address;//输出数据的地址再次输出地址,确保数据准确
asm("nop");//delay5us(1);
ALE_off;
//delay5us(1);
CAN_cs_off;
WR_off;
asm("nop");//delay5us(1);
asm("nop");
//DDRA=0xff;
PORTA=data;//输出数据
asm("nop");//delay5us(2);
WR_on;
PORTA=data;//再次输出数据,取保一致
CAN_cs_on;
asm("nop");//delay5us(2);
asm("nop");
//dog();
}
现在可以读写SJA1000了。
配置SJA1000需要使SJA1000进入复位模式,然后对一些寄存器写入数据。在这里,CAN使用Pelican模式,速率为5K,双滤波工作,
//*************************CAN复位初始化********************//
voidCAN_Init(void)
{uchari_temp=0,j_temp=0;
CLI();
//Read_SJA1000(CAN_IR);//读中断寄存器,清除中断位
Write_SJA1000(CAN_MOD,0x01);
while(!(Read_SJA1000(CAN_MOD)&0x01))//保证进入复位模式,bit0.0不为1,再写CAN_MOD
{
Write_SJA1000(CAN_MOD,0x01);
dog();
}
Write_SJA1000(CAN_CDR,0xc8);//配置时钟分频寄存器-Pelican,CBP=1,
//关闭TX1中断与时钟输出
Write_SJA1000(CAN_AMR0,0xff);//配置验收屏蔽AMR0=0FFH
Write_SJA1000(CAN_AMR1,0x00);//配置验收屏蔽AMR1=000H
Write_SJA1000(CAN_AMR2,0xff);//配置验收屏蔽AMR2=0FFH
Write_SJA1000(CAN_AMR3,0x00);//配置验收屏蔽AMR3=000H
Write_SJA1000(CAN_ACR1,0x00);//配置验收代码ACR1=0:广播
Write_SJA1000(CAN_ACR3,addr);//配置验收代码ACR3=地址
Write_SJA1000(CAN_BTR0,0x7f);//配置总线定时--5kbps
Write_SJA1000(CAN_BTR1,0xff);
Write_SJA1000(CAN_OCR,0x1a);//配置输出控制
Write_SJA1000(CAN_EWLR,0xff);//配置错误报警限制为255
do
{
Write_SJA1000(CAN_MOD,0x00);//进入工作模式双滤波
dog();
}
while((Read_SJA1000(CAN_MOD))&0x01);//确认复位标志是否被删除
Write_SJA1000(CAN_TXB+4,ID3);//配置发送缓冲区的ID3-
Write_SJA1000(CAN_IER,0x07);//配置SJA10000中断-错误报警/发送/接收中断
SEI();
}
在这之前,需要获取设备的地址,就是读取拨码开关各个脚的电平。需要注意的是,SJA1000使用的是双滤波模式,响应地址有:广播的:0x00,还有自己的地址:0x**。为什么要这么做呢,一个系统中,主机的地址一般是0X00,从机地址从0X01开始,这里面如果有两个从机的地址一样,就很可能产生一些混乱。从机一旦多了起来,查找地址相同的设备就有些麻烦了。
在程序的初始化的时候,进行SJA1000的配置。
第三部:工作程序
接下来,做的工作就是CAN试发送,别小看这个试发送,这可是解决地址重复的问题的哦,还能检测CAN网络是否正常。
//****************CAN第一次发送通讯地址测试2e*****************//
voidCAN_first_send(void)
{//ucharadd_temp=0;
uchara_temp=0;
ucharSR_temp;
asm("nop");//延时
NET_LED_on;//打开网络灯
do
{
a_temp=Read_SJA1000(CAN_SR);//读CAN_SR,直到SR.2=1:CPU可以发送数据
dog();
}
while(!(a_temp&0x04))
CLI();//关CAN中断,即总中断
Write_SJA1000(CAN_TXB+0,0xc0);//发送远程帧0xc0
Write_SJA1000(CAN_TXB+1,0x00);//发送转接器地址
Write_SJA1000(CAN_TXB+2,addr);//发送传感器地址
Write_SJA1000(CAN_TXB+3,0x2e);//发送命令码0x2e
Write_SJA1000(CAN_TXB+4,ID3);//发送ID3
Write_SJA1000(CAN_CMR,0x01);//启动发送,
//网络故障错误在中断中处理,短接H、L,按复位,先亮绿灯,后黄灯亮
asm("nop");
//SEI();
}
SJA1000的中断引脚接到MEGA16的INT1上,需要在程序初始化的时候,配置一些INT1,使MCU能响应SJA1000的中断。
数据发送前,点亮网络指示灯,什么时候熄灭它呢,在发送中断中熄灭它。
下面看看MCU对SJA1000中断的一些处理:在这里只处理:接收中断、发送中断、总线关闭中断。
#pragmainterrupt_handlercan_int:3
voidcan_int(void)
{
asm("nop");
CAN_IR_temp=Read_SJA1000(CAN_IR);//读取中断寄存器
if(CAN_IR_temp&0x01)//接收中断
{
Get_RXB_temp();
if(RxBuffer[0]==0x80)//地址测试数据帧
{
reload();//数据帧中有和自己相同的地址
}
if(RxBuffer[0]==0xc0)//远程帧则释放接收缓冲区
{
type=RxBuffer[3];//读命令码
//处理命令码
if(type==0x30)
{if(type==0x34)
{CAN_now_value_send();type=0;}//传瞬时值数据
if(type==0x27)
{reload();type=0;}//装置复位
if(type==0x2e)
{active();type=0;}//通讯地址测试
}
Write_SJA1000(CAN_CMR,0x04);//释放接收缓冲区
}
if(CAN_IR_temp&0x02)//发送中断
{
NET_LED_off;//关闭网络灯
ERR_LED_off;//关闭故障灯
CANBE_JSQ=0;//复位总线关闭计数器
asm("nop");
}
if(CAN_IR_temp&0x04)//错误报警中断(仅有总线关闭处理)
{//读状态寄存器,SR.7总线关闭:CAN控制器不参与总线活动
CAN_SR_temp=Read_SJA1000(CAN_SR);
if(CAN_SR_temp&0x80)
{
CANBE_JSQ=CANBE_JSQ+1;//关闭次数加1
if(CANBE_JSQ<CANBE_C)//关闭次数小于设定值
{
do
{
Write_SJA1000(CAN_MOD,0x00);//重新进入工作模式
}
while((Read_SJA1000(CAN_MOD))&0x01);//等待进入工作模式
Write_SJA1000(CAN_CMR,0x01);//启动CAN重新发送
}
if(CANBE_JSQ>=CANBE_C)//总线关闭次数到达设定次数
{
NET_LED_off;//关闭网络灯
ERR_LED_on;//打开故障灯
CANBE_JSQ=0;//复位总线关闭计数器
do
{
Write_SJA1000(CAN_MOD,0x00);//重新进入工作模式
}
while((Read_SJA1000(CAN_MOD))&0x01);//等待进入工作模式
Write_SJA1000(CAN_CMR,0x01);//启动CAN重新发送
CANBE_JSQ=CANBE_C;//防止CANBE_JSQ溢出
}
}
asm("nop");
}
}
中断程序中,对命令码等于0x2e的处理程序是:active();
active()程序如下:
//************************通讯地址测试2EH***********************//
voidactive(void)
{
uchartemp1,temp2;
asm("nop");//延时
NET_LED_on;//打开网络灯
CLI();//关CAN中断,即总中断
do
{
temp1=Read_SJA1000(CAN_SR);//读CAN_SR,直到SR.2=1:CPU可以发送数据
dog();
}
while(!(temp1&0x04));
Write_SJA1000(CAN_TXB+0,0x80);//发送数据帧0x80
temp2=Read_SJA1000(CAN_RXB+1);
Write_SJA1000(CAN_TXB+1,temp2);//发送转接器地址
Write_SJA1000(CAN_TXB+2,addr);//发送传感器地址
Write_SJA1000(CAN_TXB+3,0x2e);//发送命令码0x2e
Write_SJA1000(CAN_TXB+4,ID3);//发送ID3
Write_SJA1000(CAN_CMR,0x01);//启动发送
SEI();//开中断
asm("nop");
}
大家仔细看看active()程序的内容,发送了一个没有数据的数据帧:0X80,再回过头看看中断处理函数,里面有这段程序,if(RxBuffer[0]==0x80)//地址测试数据帧
{
reload();//数据帧中有和自己相同的地址
}
reload();程序很简单,就是停止喂狗,等待复位。复位之后呢,它会进行试发送,哈哈,接下来的两个地址相同的设备就“打架”起来了,现象就是一个设备不断复位,一个设备通讯灯不断闪烁。怎么样,很容易就判断出哪两个地址重复了。
命令码等于0x27时,设备复位,一般是主机发送这个远程帧。
0x34时,发送数据:
//************************瞬时值发送34H*********************//
voidCAN_now_value_send(void)
{
//uchara_temp=0;
ucharc_temp=0;
js_now_send_value();//计算需要发送的瞬间数值
asm("nop");//延时
NET_LED_on;//打开网络灯
do
{
b_temp=Read_SJA1000(CAN_SR);//读CAN_SR,直到SR.2=1:CPU可以发送数据
dog();
}
while(!(b_temp&0x04))
CLI();//关CAN中断,即总中断
Write_SJA1000(CAN_TXB+0,0x84);//发送数据帧0x84
Write_SJA1000(CAN_TXB+1,RxBuffer[1]);//发送转接器地址
Write_SJA1000(CAN_TXB+2,addr);//发送传感器地址
Write_SJA1000(CAN_TXB+3,0x34);//发送命令码0x34
Write_SJA1000(CAN_TXB+4,ID3);//发送ID3
Write_SJA1000(CAN_TXB+5,CBDJ_Send_L);//
Write_SJA1000(CAN_TXB+6,CBDJ_Send_H);//
Write_SJA1000(CAN_TXB+7,GD_Send_L);//
Write_SJA1000(CAN_TXB+8,GD_Send_H);//
Write_SJA1000(CAN_CMR,0x01);//启动发送
SEI();//开中断
asm("nop");
}
发送了一个数据帧,这个数据帧有四字节的数据。
CAN的数据帧最多支持有8个字节的数据帧,如果数据较多,可以分为多个数据帧,在命令码里面区分这些数据帧。
第四步:建立自己的CAN通讯网络。
主机可以是一台有CAN接口的计算机,一般在计算机上装一个CAN接口卡,有ISA接口的,比如PCL-841;PCI接口的。CAN卡的销售商都会提供驱动,依靠驱动里面的函数,来控制CAN卡,此项不是专长,不好多说,反正就是这个思路。
好了,昨天从南京回来的路上,就考虑发个CAN的东西。咱们这个论坛,目前还没有多少关于CAN的帖子,意在抛砖引玉…………本坛高手很多,尤其是有很多潜水的高高手~~~~
--------------------
程序中的一些DEFINE
//******************引脚信号定义***************************//
#defineCS_1(PORTB|=(1<<4))//AD7705片选
#defineCS_0(PORTB&=~(1<<4))
#defineDRDY(PINB&0x08)//AD转换DRDY信号输入
#defineNET_LED_off(PORTB|=(1<<0))//网络故障灯高电平,熄灭
#defineNET_LED_on(PORTB&=~(1<<0))//网络故障灯低电平,点亮
#defineERR_LED_off(PORTB|=(1<<1))//装置故障灯高电平,熄灭
#defineERR_LED_on(PORTB&=~(1<<1))//装置故障灯低电平,点亮
#defineDOG_on(PORTB|=(1<<2))//看门狗高
#defineDOG_off(PORTB&=~(1<<2))//看门狗低
#defineWR_on(PORTD|=(1<<0))//WR高
#defineWR_off(PORTD&=~(1<<0))//WR低
#defineRD_on(PORTD|=(1<<1))//RD高
#defineRD_off(PORTD&=~(1<<1))//RD低
#defineCAN_cs_on(PORTD|=(1<<4))//CAN高
#defineCAN_cs_off(PORTD&=~(1<<4))//CAN低
#defineALE_on(PORTD|=(1<<2))//ALE高
#defineALE_off(PORTD&=~(1<<2))//ALE低
#defineFALSE0
#defineTRUE1
#defineCANBE_C6//总线关闭次数设定值
//*******************CAN寄存器地址**************************//
#defineCAN_MOD0//模式寄存器
#defineCAN_CMR1//命令寄存器只写
#defineCAN_SR2//状态寄存器只读
#defineCAN_IR3//中断寄存器只读
#defineCAN_IER4//中断使能寄存器
#defineCAN_BTR06//总线定时寄存器0
#defineCAN_BTR17//总线定时寄存器1
#defineCAN_OCR8//输出控制寄存器
#defineCAN_TEST9//测试寄存器
#defineCAN_ALC11//仲裁丢失寄存器
#defineCAN_ECC12//错误代码捕捉寄存器
#defineCAN_EWLR13//错误报警限制寄存器
#defineCAN_EXERR14//RX错误计数寄存器
#defineCAN_TXERR15//TX错误计数寄存器
#defineCAN_ACR016//验收码寄存器0
#defineCAN_ACR117//验收码寄存器1
#defineCAN_ACR218//验收码寄存器2
#defineCAN_ACR319//验收码寄存器3
#defineCAN_AMR020//验收屏蔽寄存器0
#defineCAN_AMR121//验收屏蔽寄存器1
#defineCAN_AMR222//验收屏蔽寄存器2
#defineCAN_AMR323//验收屏蔽寄存器3
#defineCAN_TXB16//发送缓冲区首地址(工作模式)
#defineCAN_RXB16//接收缓冲区首地址(工作模式)
#defineCAN_RMC29//RX信息计数器
#defineCAN_RBSA30//RX缓冲区起始地址寄存器
#defineCAN_CDR31//时钟分频器
#defineID300//ID3
-----------------------------
初始化程序
ucharmain_ch=0;
IO_Init();//I/O口初始化
INT1_Init();
GET_add();//获取地址,地址为0,反复获取地址,直到不为0。
NET_LED_on;
ERR_LED_on;//初始化中,点亮故障灯和通讯灯,
delay50ms(2);
dog();
delay50ms(2);
dog();
delay50ms(2);
dog();
CAN_Init();//CAN初始化
NET_LED_off;
ERR_LED_off;
SEI();
CAN_first_send();//CAN试发送
delay50ms(1);
dog();
voidGET_add(void)//地址获取程序
{
ucharadd_temp=0,add_temp1=0,add_temp2=0,add_temp3=0,addr_temp=0;
do
{
dog();
NET_LED_on;
ERR_LED_on;
add_temp1=PINC&0xc3;
add_temp2=add_temp1>>4;
add_temp1=add_temp1&0x03;
add_temp3=(PIND&0xe0)>>1;
add_temp=add_temp1+add_temp2+add_temp3;
add_temp=(~add_temp)&0x7f;
addr=add_temp;
delay50ms(2);
}
while(addr==0);
}