一:MODBUS简介
modbus是一种通讯协议,是最常用的串口协议,标准的数据帧格式如下
Modbus数据帧格式
地址域:
第一个字节,每个从机都有具有唯一的地址码,并且响应回送均以各自的地址码开始。主机发送的地址码表明将发送到的从机地址,而从机发送的地址码表明回送的从机地址,地址码为0时是广播模式
功能码:
通讯传送的第二个字节。作为主机请求发送,通过功能码告诉从机执行什么动作。作为从机响应,从机发送的功能码与从主机发送来的功能码一样,并表明从机已响应主机进行操作。如果从机发送的功能码的最高位为1(比如功能码大与此同时127),则表明从机没有响应操作或发送出错。
数据:
数据区是根据不同的功能码而不同。数据区可以是实际数值、设置点、主机发送给从机或从机发送给主机的地址。
差错校验:
使用 CRC码,它是二字节的错误检测码。
部分功能码:
功能码 | 功能码意义 |
0X03 | 读取功能码 |
0X10 | 多字节写入功能码 |
本次实验使用这两个功能码
功能码详解
主设备发送读取0x03指令
0xXX | 0x03 | 0xXXXX | 0xXXXX | 0xXXXX |
设备地址 | 指令码 | 从设备起始地址 | 读取个数 | 校验 |
从设备回复读取0x03指令
0xXX | 0x03 | 0xXX | 0xXXXX*n | 0xXXXX |
设备地址 | 指令码 | 返回的字节个数 | 返回的数据 | 校验 |
主设备发送多个写入0x10指令
0xXX | 0x10 | 0xXXXX | 0xXXXX | 0xXX | 0xXXXX*n | 0xXXXX |
设备地址 | 指令码 | 从设备起始地址 | 写入几个寄存器数 | 写入个数 | 写入个数 | 校验 |
从设备回复写入0x10指令
0xXX | 0x10 | 0xXX | 0xXXXX | 0xXXXX |
设备地址 | 指令码 | 从设备起始地址 | 修改数量 | 校验 |
二:串口
本次实验使用串口DMA和串口空闲中断和串口发送完成中断.配合DMA完成主机的接受和从机的相应.使用的是STM32F103RCT6单片机,串口一的DMA通道如下图表,串口发送使用的是DMA通道4,接受使用通道5
串口空闲中断需要注意的是,产生中断后,由软件清除该位,可以定义一个变量去清楚,如:uint8_t clearIDLE;clearIDLE = USART1->SR;clearIDLE = USART1->DR,记住顺序不能反先读SR再读DR
三:DMA
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传
输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作.可编程的数据传输数目:最大为65535.
主机发送数据后,由串口DMA接受,接受到数据后,用于解析Modbus协议,根据解析后的协议做出相关功能,如:读写.并且接受完毕后则打开DMA发送,从机发送拼接好的数据帧,发送完毕后这关闭发送DMA
四:代码部分
头文件部分:
#ifndef MODBUS_H
#define MODBUS_H
#include "stm32f10x.h" // Device header
typedef struct{
uint8_t dev_addr; //从机头码
uint8_t len; //接受数据长度
uint8_t receive_data[256]; //接受的数据数组
uint8_t receive_flag; //接受使能
uint8_t send_data[256]; //发送数据数组
uint8_t send_flag; //发送使能
uint16_t rcrc; //接受到的crc
uint16_t crc; //计算接受到的crc
}Modbus_t;
void Serial_Init(void);
void SerialDMA_Init(void);
void Clean_DMA(void);
void Serial_DMA_Send(uint8_t *arr,uint8_t len);
void Modbus_Init(Modbus_t *modbusdata);
uint16_t Modbus_CRC16(uint8_t *ch,uint8_t len);
void Modbus_EventLoop(Modbus_t *modbusdata);
void Modbus_Read(Modbus_t *modbusdata);
void Modbus_Write(Modbus_t *modbusdata);
void Modbus_CRCRERR(Modbus_t *modbusdata);
void Modbus_ADDRERR(Modbus_t *modbusdata);
#endif
串口和DMA部分:
Modbus_t mod //定义一个全局变量用于储存Modbus数据
//以下数组用于储存模拟的从机数据
uint16_t Reg[] = {0x6677,0x8899,0x99ff,0xaabb,0xbbcc,0xccdd,0xabcd,0xdcef,0xffff,0x7fff,
0x6789,0xdddd,0xeeee,0xfefe,0xaaaa,0xbbbb,0xcccc,0x1010,0x1111,0x011f};
/*
*串口初始化 波特率 奇偶位 流控位 数据位等
*/
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
GPIO_InitTypeDef serialgpio = {
.GPIO_Mode = GPIO_Mode_AF_PP,
.GPIO_Pin = GPIO_Pin_9,
.GPIO_Speed = GPIO_Speed_50MHz,
};
GPIO_Init(GPIOA,&serialgpio);
serialgpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
serialgpio.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA,&serialgpio);
USART_InitTypeDef serial = {
.USART_BaudRate = 115200,
.USART_HardwareFlowControl = USART_HardwareFlowControl_None,
.USART_Mode = USART_Mode_Rx | USART_Mode_Tx,
.USART_Parity = USART_Parity_No,
.USART_StopBits = USART_StopBits_1,
.USART_WordLength = USART_WordLength_8b,
};
USART_Init(USART1,&serial);
//串口空闲中断使能
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
//串口发送完成使能
USART_ITConfig(USART1,USART_IT_TC,ENABLE);
USART_Cmd(USART1,ENABLE);
//串口中断配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef serialnvic = {
.NVIC_IRQChannelPreemptionPriority = 1,
.NVIC_IRQChannelSubPriority = 1,
.NVIC_IRQChannelCmd = ENABLE,
.NVIC_IRQChannel = USART1_IRQn,
};
NVIC_Init(&serialnvic);
}
/*
* 串口DMA配置
* 外设地址 外设数据长度 外设地址是否自增
* 内存地址 内存地址长度 内存地址是否自增
* DMA缓冲区大小 DMA优先级 DMA模式 DMA数据传输方向 DMA是否开启内存到内存
*/
void SerialDMA_Init(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_DeInit(DMA1_Channel5);
DMA_InitTypeDef rxdma = {
.DMA_PeripheralBaseAddr = USART1_BASE + 0x04, //串口外设地址 与可以直接写0x40013804或(u32)&USART1->DR
.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte, //外设数据长度 8位
.DMA_PeripheralInc = DMA_PeripheralInc_Disable, //外设地址不自增
.DMA_MemoryBaseAddr = (u32)&mod.receive_data, //内存地址
.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte, //内存数据长度 8位
.DMA_MemoryInc = DMA_MemoryInc_Enable, //内存地址自增
.DMA_BufferSize = 256, //dma缓冲区大小
.DMA_Priority = DMA_Priority_High, //优先级高
.DMA_Mode = DMA_Mode_Normal, //不开启循环模式
.DMA_DIR = DMA_DIR_PeripheralSRC, //数据方向为 外设-->内存 及USART1 --> mod.receive_data
.DMA_M2M = DMA_M2M_Disable, //关闭内存到内存
};
DMA_Init(DMA1_Channel5,&rxdma);
DMA_DeInit(DMA1_Channel4);
DMA_InitTypeDef txdma = {
.DMA_PeripheralBaseAddr = USART1_BASE + 0x04,
.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte,
.DMA_PeripheralInc = DMA_PeripheralInc_Disable,
.DMA_MemoryBaseAddr = (u32)&mod.send_data,
.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte,
.DMA_MemoryInc = DMA_MemoryInc_Enable,
.DMA_BufferSize = 0, //dma缓冲区为0,是因为发送数据由主机指令来动态分配
.DMA_Priority = DMA_Priority_High,
.DMA_Mode = DMA_Mode_Normal,
.DMA_DIR = DMA_DIR_PeripheralDST, //数据方向 因为是发送 所以为 内存到外设 及 mod.send_data --> USART1
.DMA_M2M = DMA_M2M_Disable,
};
DMA_Init(DMA1_Channel4,&txdma);
USART_DMACmd(USART1,USART_DMAReq_Rx | USART_DMAReq_Tx,ENABLE); //开启串口DMA数据发送和接受
DMA_Cmd(DMA1_Channel4,DISABLE); //关闭DMA发送
DMA_Cmd(DMA1_Channel5,ENABLE); //开启DMA接受
}
Modbus部分:
/*
* modbus初始化
*/
void Modbus_Init(Modbus_t *modbusdata)
{
modbusdata->dev_addr = 0x02; //从机地址为0x02
modbusdata->receive_flag = 0;
modbusdata->len = 0;
modbusdata->send_flag = 0;
Serial_Init();
SerialDMA_Init();
}
/*
*CRC校验
*/
uint16_t Modbus_CRC16(uint8_t *ch,uint8_t len)
{
uint16_t crc = 0xffff,code = 0xa001;
char i,j;
for(i = 0;i< len;i++){
crc ^= ch[i];
for(j = 0;j < 8;j++){
if(crc&1){
crc>>=1;
crc^=code;
}else{
crc>>=1;
}
}
}
return crc;
}
/*
*modbus事件轮询
*/
void Modbus_EventLoop(Modbus_t *modbusdata)
{
if(modbusdata->receive_flag == 0) //判断是否接受完成
return;
if(modbusdata->receive_data[0] == modbusdata->dev_addr) //判断头码
{
modbusdata->rcrc = modbusdata->receive_data[modbusdata->len-1];
modbusdata->rcrc<<=8;
modbusdata->rcrc |= modbusdata->receive_data[modbusdata->len-2];
modbusdata->crc = Modbus_CRC16(modbusdata->receive_data,modbusdata->len-2);
if(modbusdata->crc == modbusdata->rcrc) //判断CRC
{
switch (modbusdata->receive_data[1]) //判断功能码
{
case 0x03:Modbus_Read(modbusdata); //功能码事件
break;
case 0x10:Modbus_Write(modbusdata); //功能码事件
break;
default:
break;
}
}
else
{
Modbus_CRCRERR(modbusdata); //CRC错误帧
}
}
else
{
Modbus_ADDRERR(modbusdata); //头码错误帧
}
modbusdata->receive_flag = 0;
modbusdata->send_flag = 0;
}
/*
*清除接收通道,让DMA从头开始计数,如果不清除,每次接收完成之后为了使下次的接收是从内存中的下标0
*存储,需要重新写入要传输的数据数量,否则下次直接接着上次传输的位置开始接收存储,发送时也一样
*/
void Clean_DMA(void)
{
DMA_Cmd(DMA1_Channel5,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5,sizeof(mod.receive_data));
DMA_Cmd(DMA1_Channel5,ENABLE);
}
/*
*发送缓冲区数据
*/
void Serial_DMA_Send(uint8_t *arr,uint8_t len)
{
if(len == 0)
return;
while(DMA_GetCurrDataCounter(DMA1_Channel4));
DMA_Cmd(DMA1_Channel4,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel4,len);
DMA_Cmd(DMA1_Channel4,ENABLE);
}
//以下为功能码事件函数 和 错误码函数
void Modbus_Read(Modbus_t *modbusdata)
{
uint8_t i,j;
uint16_t crc,start_addr,read_len;
start_addr = modbusdata->receive_data[2];
start_addr <<=8;
start_addr |= modbusdata->receive_data[3];
read_len = modbusdata->receive_data[4];
read_len <<=8;
read_len |= modbusdata->receive_data[5];
i=0;
modbusdata->send_data[i++] = modbusdata->dev_addr;
modbusdata->send_data[i++] = 0x03;
modbusdata->send_data[i++] = (read_len * 2)%256;
for(j=0;j<read_len;j++)
{
modbusdata->send_data[i++] = Reg[start_addr+j]>>8;
modbusdata->send_data[i++] = Reg[start_addr+j];
}
crc = Modbus_CRC16(modbusdata->send_data,i);
modbusdata->send_data[i++] = crc>>8;
modbusdata->send_data[i++] = crc;
Serial_DMA_Send(modbusdata->send_data,i);
}
void Modbus_Write(Modbus_t *modbusdata)
{
uint8_t i;
uint16_t crc,start_addr,write_num;
start_addr = modbusdata->receive_data[2];
start_addr <<=8;
start_addr |= modbusdata->receive_data[3];
write_num = modbusdata->receive_data[4];
write_num <<=8;
write_num |= modbusdata->receive_data[5];
for(i=0;i<write_num;i++)
{
Reg[start_addr+i] = modbusdata->receive_data[7+i*2];
Reg[start_addr+i] <<=8;
Reg[start_addr+i] |= modbusdata->receive_data[8+i*2];
}
modbusdata->send_data[0] = modbusdata->dev_addr;
modbusdata->send_data[1] = 0x10;
modbusdata->send_data[2] = start_addr>>8;
modbusdata->send_data[3] = start_addr;
modbusdata->send_data[4] = write_num>>8;
modbusdata->send_data[5] = write_num;
crc = Modbus_CRC16(modbusdata->send_data,6);
modbusdata->send_data[6] = crc>>8;
modbusdata->send_data[7] = crc;
Serial_DMA_Send(modbusdata->send_data,8);
}
void Modbus_ADDRERR(Modbus_t *modbusdata)
{
uint16_t crc;
modbusdata->send_data[0] = 0x81;
modbusdata->send_data[1] = 0x66;
modbusdata->send_data[2] = 0x00;
modbusdata->send_data[3] = 0x02;
modbusdata->send_data[4] = modbusdata->receive_data[0]>>8;
modbusdata->send_data[5] = modbusdata->receive_data[0];
crc = Modbus_CRC16(modbusdata->send_data,6);
modbusdata->send_data[6] = crc>>8;
modbusdata->send_data[7] = crc;
Serial_DMA_Send(modbusdata->send_data,8);
}
void Modbus_CRCRERR(Modbus_t *modbusdata)
{
uint16_t crc;
modbusdata->send_data[0] = 0x81;
modbusdata->send_data[1] = 0x65;
modbusdata->send_data[2] = modbusdata->rcrc>>8;
modbusdata->send_data[3] = modbusdata->rcrc;
modbusdata->send_data[4] = modbusdata->crc>>8;
modbusdata->send_data[5] = modbusdata->crc;
crc = Modbus_CRC16(modbusdata->send_data,6);
modbusdata->send_data[6] = crc>>8;
modbusdata->send_data[7] = crc;
Serial_DMA_Send(modbusdata->send_data,8);
}
中断和主函数部分:
//主函数部分
extern Modbus_t mod;
int main(void)
{
Modbus_Init(&mod);
while (1)
{
Modbus_EventLoop(&mod);
}
}
void USART1_IRQHandler(void)
{
uint8_t clear;
if(USART_GetITStatus(USART1,USART_IT_IDLE) == SET)
{
clear = USART1->SR;
clear = USART1->DR;
clear = clear; //防止编译器警告
mod.receive_flag = 1; //接受完成标准位 置一
mod.len = sizeof(mod.receive_data) - DMA_GetCurrDataCounter(DMA1_Channel5); //获取接受数据长度
Clean_DMA();
}
if(USART_GetITStatus(USART1,USART_IT_TC) == SET)
{
USART_ClearITPendingBit(USART1,USART_IT_TC);
mod.send_flag = 1; //发送完成标准位 置一
DMA_Cmd(DMA1_Channel4,DISABLE);
}
}