在做modbus RTU串口从机的时候,多字节收发总是出现错误。或者发送间隔过长,严重影响轮询速度,于是自己动手写了一个单函数的modbus协议,支持03和06命令字,多字节读,单字节写,理论轮询间隔在20ms甚至10ms,50ms测试1000次无错误。
发送间隔50ms
@near uchar data1[10];//串口数据缓存
@far @interrupt void UART1_int (void)//串口收中断
{
uchar uarti;
for(uarti=0;uarti<7;uarti++)
{
data1[uarti]=data1[uarti+1];
}
UART1_SR_RXNE = 0;
data1[7]=UART1_DR;
Printmodbus(data1,ddd);
}
该程序写得比较有针对性,比较适合小单片机低RAM使用,如果纯粹作为modbus从机数据传输,也基本够用,当然多字节写等其它命令字也是可以扩展的。
//CRC_16计算函数,注意这是stm8s大端,如果计算错误,请用下面第二个小端程序。
unsigned short int CRC_16(unsigned char *data,int len)
{
unsigned char *buf;
unsigned short int * CRC;
unsigned short int crch,crcl;
uchar p;
uchar j;
char err;
buf= & data[len];
CRC=(unsigned short int *)buf;
buf[0]=0xff;//lsb
buf[1]=0xff;//msb
for(p=0;p<len;p++)
{
buf[1]=buf[1]^data[p];
for(j=0;j<8;j++)
{
err=buf[1]&1;
*CRC=*CRC/2;
if(err) *CRC=*CRC^0xa001;
}
}
crch=*CRC>>8;
crcl=*CRC<<8;
*CRC=crch+crcl;
return(*CRC);
}
//CRC16第二类计算
unsigned short int CRC_16(unsigned char *data,int len)
{
unsigned char *buf;
unsigned short int * CRC;
unsigned short int crch,crcl;
short int i;
short int j;
char err;
buf= & data[len];
CRC=(unsigned short int *)buf;
buf[0]=0xff;
buf[1]=0xff;
for(i=0;i<len;i++)
{
buf[0]=buf[0]^data;
for(j=0;j<8;j++)
{
err=buf[0]&1;
*CRC=*CRC/2;
if(err) *CRC=*CRC^0xa001;
}
}
crch=*CRC>>8;
crcl=*CRC<<8;
*CRC=crch+crcl;
return(*CRC);
}
//简化版modbus协议单函数
//temp[]为串口收到命令,data为要发送数据。
unsigned char ddd[40]={0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0};//协议数据缓存
void Printmodbus(unsigned char temp[],unsigned char data[])
{
unsigned char temp1[5];
unsigned int get,get2;
unsigned char crch,crcl;
unsigned char len=5+temp[5]*2;
unsigned char temp2[60];
unsigned char temp3[8];
unsigned char m,n,g;
temp1[0]=temp[0];temp1[1]=temp[1];temp1[2]=temp[2];
temp1[3]=temp[3];temp1[4]=temp[4];temp1[5]=temp[5];
get=CRC_16(temp1,6);
crch=(unsigned char)(get>>8);
crcl=(unsigned char)(get);
if(temp[6]==crch&&temp[7]==crcl)
{
UART1_CR2_RIEN=0; //关串口中断,重要
if((temp[0]==0)||(temp[0]==data[39])) //判断地址匹配,协定0号地址为广播地址。data【39】存储设备地址,值在设备启动时从eeprom加载
{
switch(temp[1])
{
case 3: //读取命令字
temp2[0]=data[39];
temp2[1]=temp[1];
temp2[2]=temp[5]*2;
for( m=3;m<len-2;m++)
{
temp2[m]=data[m-3+temp1[3]*2];
}
get2=CRC_16(temp2,len-2);
temp2[len-2]=(unsigned char)(get2>>8);
temp2[len-1]=(unsigned char)(get2);
PD4=1; //PD1为MAX485芯片控制引脚
PD4=1;
delay_ms(1);
for(n=0;n<len;n++)
{
uart1_send(temp2[n]); //发送返回字串
}
delay_ms(5); //如果需要返回更长字节,每增加10个字节需要增加1ms延时以保证数据发送完整。
PD4=0;
PD4=0;
UART1_CR2_RIEN=1; //打开串口中断
break;
case 6://修改命令字,为了防止寄存器数据被误修改,这里设定400018寄存器为锁定位,锁定密码为0x18,要修改其他寄存器需先将此位赋值0x18;
if(temp[3]*2==34) //如果是修改400018寄存器则可直接修改
{
ddd[temp[3]*2]=temp[4];
ddd[temp[3]*2+1]=temp[5]; //这两条为寄存器修改
if((ddd[temp[3]*2]==temp[4])&&(ddd[temp[3]*2+1]==temp[5]))
{
temp3[0]=data[39];
temp3[1]=temp[1];
temp3[2]=temp[2];
temp3[3]=temp[3];
temp3[4]=temp[4];
temp3[5]=temp[5];
get2=CRC_16(temp3,6);
temp3[6]=(unsigned char)(get2>>8);
temp3[7]=(unsigned char)(get2);
PD4=1;
PD4=1;
delay_ms(1);
for(n=0;n<8;n++)
{
uart1_send(temp3[n]);
}
delay_ms(5);
PD4=0;
PD4=0;
}
}
else if(data[35]==0x18) //非400018寄存器则先判断锁定位是否解锁
{
uint addsh,addsl;
addsh=0x4000+temp[3]*2;
addsl=0x4001+temp[3]*2;
__eeprom_write_8(addsh,temp[4]); // 将固定参数写入对应EEPROM,考虑eeprom寿命问题,请不要将变化平凡的值写入eeprom
__eeprom_write_8(addsl,temp[5]); //将固定参数写入对应EEPROM
ddd[temp[3]*2]=eep_read(addsh);
ddd[temp[3]*2+1]=eep_read(addsl); //这两条为寄存器修改
if((ddd[temp[3]*2]==temp[4])&&(ddd[temp[3]*2+1]==temp[5]))
{
temp3[0]=data[39];
temp3[1]=temp[1];
temp3[2]=temp[2];
temp3[3]=temp[3];
temp3[4]=temp[4];
temp3[5]=temp[5];
get2=CRC_16(temp3,6);
temp3[6]=(unsigned char)(get2>>8);
temp3[7]=(unsigned char)(get2);
PD4=1;
PD4=1;
delay_ms(1);
for(n=0;n<8;n++)
{
//printf("%x\n",temp3[n]);
uart1_send(temp3[n]); //修改成功发送返回值
}
delay_ms(5);
PD4=0;
PD4=0;
}
}
UART1_CR2_RIEN=1; //打开串口中断
break;
UART1_CR2_RIEN=1; //打开串口中断
}
}
}
else
{
UART1_CR2_RIEN=1; //打开串口中断
}
UART1_CR2_RIEN=1; //打开串口中断
}