串口1中断收发-C语言-MODBUS协议

https://www.stcaimcu.com/forum.php?mod=viewthread&tid=375&extra=page%3D1

本文介绍了一个基于STC8G系列单片机的MODBUS-RTU从机通信程序。代码实现了串口1中断收发功能,支持MODBUS协议的读写多寄存器操作(功能码03H和10H),并定义64个寄存器(地址0x1000~0x103F)。程序特点包括:1)采用11.0592MHz主频,9600bps波特率;2)使用定时器0实现35位时间的接收超时检测;3)数据采用大端模式,CRC16校验为小端模式;4)支持错误响应(功能码错误、长度错误等);5)提供寄存器读写示例。代码包含初始化配置、CRC计算、协议解析及中断处理等模块,适用于工业控制等需要MODBUS通信的场景

/*********************************************************/

#define MAIN_Fosc 11059200L //定义主时钟

#include "..\..\STC8Gxxx.h"

/************* 功能说明 **************

请先别修改程序, 直接下载"08-串口1中断收发-C语言-MODBUS协议"里的"UART1.hex"测试, 主频选择11.0592MHZ. 测试正常后再修改移植.

串口1按MODBUS-RTU协议通信. 本例为从机程序, 主机一般是电脑端.

本例程只支持多寄存器读和多寄存器写, 寄存器长度为64个, 别的命令用户可以根据需要按MODBUS-RTU协议自行添加.

本例子数据使用大端模式(与C51一致), CRC16使用小端模式(与PC一致).

默认参数:

串口1设置均为 1位起始位, 8位数据位, 1位停止位, 无校验.

串口1(P3.0 P3.1): 9600bps.

定时器0用于超时计时. 串口每收到一个字节都会重置超时计数, 当串口空闲超过35bit时间时(9600bps对应3.6ms)则接收完成.

用户修改波特率时注意要修改这个超时时间.

本例程只是一个应用例子, 科普MODBUS-RTU协议并不在本例子职责范围, 用户可以上网搜索相关协议文本参考.

本例定义了64个寄存器, 访问地址为0x1000~0x103f.

命令例子:

写入4个寄存器(8个字节):

10 10 1000 0004 08 1234 5678 90AB CDEF 4930

返回:

10 10 10 00 00 04 4B C6

读出4个寄存器:

10 03 1000 0004 4388

返回:

10 03 08 12 34 56 78 90 AB CD EF 3D D5

命令错误返回信息(自定义):

0x90: 功能码错误. 收到了不支持的功能码.

0x91: 命令长度错误.

0x92: 写入或读出寄存器个数或字节数错误.

0x93: 寄存器地址错误.

注意: 收到广播地址0x00时要处理信息, 但不返回应答.

******************************************/

/************* 本地常量声明 **************/

#define RX1_Length 128 /* 接收缓冲长度 */

#define TX1_Length 128 /* 发送缓冲长度 */

/************* 本地变量声明 **************/

u8 xdata RX1_BufferRX1_Length; //接收缓冲

u8 xdata TX1_BufferTX1_Length; //发送缓冲

u8 RX1_cnt; //接收字节计数.

u8 TX1_cnt; //发送字节计数

u8 TX1_number; //要发送的字节数

u8 RX1_TimeOut; //接收超时计时器

bit B_RX1_OK; // 接收数据标志

bit B_TX1_Busy; // 发送忙标志

/************* 本地函数声明 **************/

void UART1_config(u32 brt, u8 timer, u8 io); // brt: 通信波特率, timer=2: 波特率使用定时器2, 其它值: 使用Timer1做波特率. io=0: 串口1切换到P3.0 P3.1, =1: 切换到P3.6 P3.7, =2: 切换到P1.6 P1.7, =3: 切换到P4.3 P4.4.

u8 Timer0_Config(u8 t, u32 reload); //t=0: reload值是主时钟周期数, t=1: reload值是时间(单位us), 返回0正确, 返回1装载值过大错误.

u16 MODBUS_CRC16(u8 *p, u8 n);

u8 MODBUS_RTU(void);

#define SL_ADDR 0x10 /* 本从机站号地址 */

#define REG_ADDRESS 0x1000 /* 寄存器首地址 */

#define REG_LENGTH 64 /* 寄存器长度 */

u16 xdata modbus_regREG_LENGTH; /* 寄存器地址 */

//========================================================================

// 函数: void main(void)

// 描述: 主函数

// 参数: none.

// 返回: none.

// 版本: VER1.0

// 日期: 2018-4-2

// 备注:

//========================================================================

void main(void)

{

u8 i;

u16 crc;

Timer0_Config(0, MAIN_Fosc / 10000); //t=0: reload值是主时钟周期数, (中断频率, 20000次/秒)

UART1_config(9600UL, 1, 0); // brt: 通信波特率, timer=2: 波特率使用定时器2, 其它值: 使用Timer1做波特率. io=0: 串口1切换到P3.0 P3.1, =1: 切换到P3.6 P3.7, =2: 切换到P1.6 P1.7, =3: 切换到P4.3 P4.4.

EA = 1;

while (1)

{

if(B_RX1_OK && !B_TX1_Busy) //收到数据, 进行MODBUS-RTU协议解析

{

if(MODBUS_CRC16(RX1_Buffer, RX1_cnt) == 0) //首先判断CRC16是否正确, 不正确则忽略, 不处理也不返回信息

{

if((RX1_Buffer0 == 0x00) || (RX1_Buffer0 == SL_ADDR)) //然后判断站号地址是否正确, 或者是否广播地址(不返回信息)

{

if(RX1_cnt > 2) RX1_cnt -= 2; //去掉CRC16校验字节

i = MODBUS_RTU(); //MODBUS-RTU协议解析

if(i != 0) //错误处理

{

TX1_Buffer0 = SL_ADDR; //站号地址

TX1_Buffer1 = i; //错误代码

crc = MODBUS_CRC16(TX1_Buffer, 2);

TX1_Buffer2 = (u8)(crc>>8); //CRC是小端模式

TX1_Buffer3 = (u8)crc;

B_TX1_Busy = 1; //标志发送忙

TX1_cnt = 0; //发送字节计数

TX1_number = 4; //要发送的字节数

TI = 1; //启动发送

}

}

}

RX1_cnt = 0;

B_RX1_OK = 0;

}

}

}

/****************************** MODBUS_CRC (shift) *************** past test 06-11-27 *********

计算CRC,调用方式 MODBUS_CRC16(&CRC,8); &CRC为首地址,8为字节数

CRC-16 for MODBUS

CRC16=X16+X15+X2+1

TEST: ---> ABCDEFGHIJ CRC16=0x0BEE 1627T

*/

//========================================================================

// 函数: u16 MODBUS_CRC16(u8 *p, u8 n)

// 描述: 计算CRC16函数.

// 参数: *p: 要计算的数据指针.

// n: 要计算的字节数.

// 返回: CRC16值.

// 版本: V1.0, 2022-3-18 梁工

//========================================================================

u16 MODBUS_CRC16(u8 *p, u8 n)

{

u8 i;

u16 crc16;

crc16 = 0xffff; //预置16位CRC寄存器为0xffff(即全为1)

do

{

crc16 ^= (u16)*p; //把8位数据与16位CRC寄存器的低位相异或,把结果放于CRC寄存器

for(i=0; i<8; i++) //8位数据

{

if(crc16 & 1) crc16 = (crc16 >> 1) ^ 0xA001; //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位,

//再异或多项式0xA001

else crc16 >>= 1; //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位

}

p++;

}while(--n != 0);

return (crc16);

}

/********************* modbus协议 *************************/

/***************************************************************************

写多寄存器

数据: 地址 功能码 寄存地址 寄存器个数 写入字节数 写入数据 CRC16

偏移: 0 1 2 3 4 5 6 7~ 最后2字节

字节: 1 byte 1 byte 2 byte 2 byte 1byte 2*n byte 2 byte

addr 0x10 xxxx xxxx xx xx....xx xxxx

返回

数据: 地址 功能码 寄存地址 寄存器个数 CRC16

偏移: 0 1 2 3 4 5 6 7

字节: 1 byte 1 byte 2 byte 2 byte 2 byte

addr 0x10 xxxx xxxx xxxx

读多寄存器

数据:站号(地址) 功能码 寄存地址 寄存器个数 CRC16

偏移: 0 1 2 3 4 5 6 7

字节: 1 byte 1 byte 2 byte 2 byte 2 byte

addr 0x03 xxxx xxxx xxxx

返回

数据:站号(地址) 功能码 读出字节数 读出数据 CRC16

偏移: 0 1 2 3~ 最后2字节

字节: 1 byte 1 byte 1byte 2*n byte 2 byte

addr 0x03 xx xx....xx xxxx

返回错误代码

数据:站号(地址) 错误码 CRC16

偏移: 0 1 最后2字节

字节: 1 byte 1 byte 2 byte

addr 0x03 xxxx

***************************************************************************/

u8 MODBUS_RTU(void)

{

u8 i,j,k;

u16 reg_addr; //寄存器地址

u8 reg_len; //写入寄存器个数

u16 crc;

if(RX1_Buffer1 == 0x10) //写多寄存器

{

if(RX1_cnt < 9) return 0x91; //命令长度错误

if((RX1_Buffer4 != 0) || ((RX1_Buffer5 *2) != RX1_Buffer6)) return 0x92; //写入寄存器个数与字节数错误

if((RX1_Buffer5==0) || (RX1_Buffer5 > REG_LENGTH)) return 0x92; //写入寄存器个数错误

reg_addr = ((u16)RX1_Buffer2 << 8) + RX1_Buffer3; //寄存器地址

reg_len = RX1_Buffer5; //写入寄存器个数

if((reg_addr+(u16)RX1_Buffer5) > (REG_ADDRESS+REG_LENGTH)) return 0x93; //寄存器地址错误

if(reg_addr < REG_ADDRESS) return 0x93; //寄存器地址错误

if((reg_len*2+7) != RX1_cnt) return 0x91; //命令长度错误

j = reg_addr - REG_ADDRESS; //寄存器数据下标

for(k=7, i=0; i<reg_len; i++,j++)

{

modbus_regj = ((u16)RX1_Bufferk << 8) + RX1_Bufferk+1; //写入数据, 大端模式

k += 2;

}

if(RX1_Buffer0 != 0) //非广播地址则应答

{

for(i=0; i<6; i++) TX1_Bufferi = RX1_Bufferi; //要返回的应答

crc = MODBUS_CRC16(TX1_Buffer, 6);

TX1_Buffer6 = (u8)(crc>>8); //CRC是小端模式

TX1_Buffer7 = (u8)crc;

B_TX1_Busy = 1; //标志发送忙

TX1_cnt = 0; //发送字节计数

TX1_number = 8; //要发送的字节数

TI = 1; //启动发送

}

}

else if(RX1_Buffer1 == 0x03) //读多寄存器

{

if(RX1_Buffer0 != 0) //非广播地址则应答

{

if(RX1_cnt != 6) return 0x91; //命令长度错误

if(RX1_Buffer4 != 0) return 0x92; //读出寄存器个数错误

if((RX1_Buffer5==0) || (RX1_Buffer5 > REG_LENGTH)) return 0x92; //读出寄存器个数错误

reg_addr = ((u16)RX1_Buffer2 << 8) + RX1_Buffer3; //寄存器地址

reg_len = RX1_Buffer5; //读出寄存器个数

if((reg_addr+(u16)RX1_Buffer5) > (REG_ADDRESS+REG_LENGTH)) return 0x93; //寄存器地址错误

if(reg_addr < REG_ADDRESS) return 0x93; //寄存器地址错误

j = reg_addr - REG_ADDRESS; //寄存器数据下标

TX1_Buffer0 = SL_ADDR; //站号地址

TX1_Buffer1 = 0x03; //读功能码

TX1_Buffer2 = reg_len*2; //返回字节数

for(k=3, i=0; i<reg_len; i++,j++)

{

TX1_Bufferk++ = (u8)(modbus_regj >> 8); //数据为大端模式

TX1_Bufferk++ = (u8)modbus_regj;

}

crc = MODBUS_CRC16(TX1_Buffer, k);

TX1_Bufferk++ = (u8)(crc>>8); //CRC是小端模式

TX1_Bufferk++ = (u8)crc;

B_TX1_Busy = 1; //标志发送忙

TX1_cnt = 0; //发送字节计数

TX1_number = k; //要发送的字节数

TI = 1; //启动发送

}

}

else return 0x90; //功能码错误

return 0; //解析正确

}

//========================================================================

// 函数:u8 Timer0_Config(u8 t, u32 reload)

// 描述: timer0初始化函数.

// 参数: t: 重装值类型, 0表示重装的是系统时钟数, 其余值表示重装的是时间(us).

// reload: 重装值.

// 返回: 0: 初始化正确, 1: 重装值过大, 初始化错误.

// 版本: V1.0, 2018-3-5

//========================================================================

u8 Timer0_Config(u8 t, u32 reload) //t=0: reload值是主时钟周期数, t=1: reload值是时间(单位us)

{

TR0 = 0; //停止计数

if(t != 0) reload = (u32)(((float)MAIN_Fosc * (float)reload)/1000000UL); //重装的是时间(us), 计算所需要的系统时钟数.

if(reload >= (65536UL * 12)) return 1; //值过大, 返回错误

if(reload < 65536UL) AUXR |= 0x80; //1T mode

else

{

AUXR &= ~0x80; //12T mode

reload = reload / 12;

}

reload = 65536UL - reload;

TH0 = (u8)(reload >> 8);

TL0 = (u8)(reload);

ET0 = 1; //允许中断

TMOD &= 0xf0;

TMOD |= 0; //工作模式, 0: 16位自动重装, 1: 16位定时/计数, 2: 8位自动重装, 3: 16位自动重装, 不可屏蔽中断

TR0 = 1; //开始运行

return 0;

}

//========================================================================

// 函数: void timer0_ISR (void) interrupt TIMER0_VECTOR

// 描述: timer0中断函数.

// 参数: none.

// 返回: none.

// 版本: V1.0, 2016-5-12

//========================================================================

void timer0_ISR (void) interrupt TIMER0_VECTOR

{

if(RX1_TimeOut != 0)

{

if(--RX1_TimeOut == 0) //超时

{

if(RX1_cnt != 0) //接收有数据

{

B_RX1_OK = 1; //标志已收到数据块

}

}

}

}

//========================================================================

// 函数: SetTimer2Baudraye(u16 dat)

// 描述: 设置Timer2做波特率发生器。

// 参数: dat: Timer2的重装值.

// 返回: none.

// 版本: VER1.0

// 日期: 2018-4-2

// 备注:

//========================================================================

void SetTimer2Baudraye(u16 dat) // 选择波特率, 2: 使用Timer2做波特率, 其它值: 使用Timer1做波特率.

{

AUXR &= ~(1<<4); //Timer stop

AUXR &= ~(1<<3); //Timer2 set As Timer

AUXR |= (1<<2); //Timer2 set as 1T mode

TH2 = (u8)(dat >> 8);

TL2 = (u8)dat;

IE2 &= ~(1<<2); //禁止中断

AUXR |= (1<<4); //Timer run enable

}

//========================================================================

// 函数: void UART1_config(u32 brt, u8 timer, u8 io)

// 描述: UART1初始化函数。

// 参数: brt: 通信波特率.

// timer: 波特率使用的定时器, timer=2: 波特率使用定时器2, 其它值: 使用Timer1做波特率.

// io: 串口1切换到的IO, io=0: 串口1切换到P3.0 P3.1, =1: 切换到P3.6 P3.7, =2: 切换到P1.6 P1.7, =3: 切换到P4.3 P4.4.

// 返回: none.

// 版本: VER1.0

// 日期: 2018-4-2

// 备注:

//========================================================================

void UART1_config(u32 brt, u8 timer, u8 io) // brt: 通信波特率, timer=2: 波特率使用定时器2, 其它值: 使用Timer1做波特率. io=0: 串口1切换到P3.0 P3.1, =1: 切换到P3.6 P3.7, =2: 切换到P1.6 P1.7, =3: 切换到P4.3 P4.4.

{

brt = 65536UL - (MAIN_Fosc / 4) / brt;

if(timer == 2) //波特率使用定时器2

{

AUXR |= 0x01; //S1 BRT Use Timer2;

SetTimer2Baudraye((u16)brt);

}

else //波特率使用定时器1

{

TR1 = 0;

AUXR &= ~0x01; //S1 BRT Use Timer1;

AUXR |= (1<<6); //Timer1 set as 1T mode

TMOD &= ~(1<<6); //Timer1 set As Timer

TMOD &= ~0x30; //Timer1_16bitAutoReload;

TH1 = (u8)(brt >> 8);

TL1 = (u8)brt;

ET1 = 0; // 禁止Timer1中断

INT_CLKO &= ~0x02; // Timer1不输出高速时钟

TR1 = 1; // 运行Timer1

}

if(io == 1) {S1_USE_P36P37(); P3n_standard(0xc0);} //切换到 P3.6 P3.7

else if(io == 2) {S1_USE_P16P17(); P1n_standard(0xc0);} //切换到 P1.6 P1.7

else if(io == 3) {S1_USE_P43P44(); P4n_standard(0x18);} //切换到 P4.3 P4.4

else {S1_USE_P30P31(); P3n_standard(0x03);} //切换到 P3.0 P3.1

SCON = (SCON & 0x3f) | (1<<6); // 8位数据, 1位起始位, 1位停止位, 无校验

// PS = 1; //高优先级中断

ES = 1; //允许中断

REN = 1; //允许接收

}

//========================================================================

// 函数: void UART1_ISR (void) interrupt UART1_VECTOR

// 描述: 串口1中断函数

// 参数: none.

// 返回: none.

// 版本: VER1.0

// 日期: 2018-4-2

// 备注:

//========================================================================

void UART1_ISR (void) interrupt UART1_VECTOR

{

if(RI)

{

RI = 0;

if(!B_RX1_OK) //接收缓冲空闲

{

if(RX1_cnt >= RX1_Length) RX1_cnt = 0;

RX1_BufferRX1_cnt++ = SBUF;

RX1_TimeOut = 36; //接收超时计时器, 35个位时间

}

}

if(TI)

{

TI = 0;

if(TX1_number != 0) //有数据要发

{

SBUF = TX1_BufferTX1_cnt++;

TX1_number--;

}

else B_TX1_Busy = 0;

}

}

相关推荐
zhixingheyi_tian43 分钟前
一份GC日志的解读
java·开发语言
apihz1 小时前
随机驾考题目(C 照科一 / 科四 2000+ 题)免费API调用教程
android·java·c语言·开发语言·网络协议·tcp/ip
matlab代码2 小时前
基于matlab人脸疲劳驾驶检测系统(可使用其它人脸视频)【源码39期】
开发语言·matlab·人脸疲劳检测
zenithdev12 小时前
fastcache:为 Go 设计的低 GC 压力内存缓存
开发语言·其他·缓存·golang
想你依然心痛3 小时前
自定义RTOS内核:从零实现上下文切换与任务调度——汇编、PendSV
java·开发语言·汇编·pendsv
geovindu3 小时前
java:Abstract Factory Pattern
java·开发语言·后端·设计模式·抽象工厂模式·创建型模式
geovindu3 小时前
java: Factory Method Pattern
java·开发语言·后端·设计模式·工厂方法模式·创建型模式
m0_587098993 小时前
Qt,二进制文件读写建议
开发语言·qt
爱敲代码的小鱼3 小时前
springsecurity:
java·开发语言·数据库