modbus协议
完成modbus协议的编程之后,设备可以分别作为modbus协议的主机或者从机进行测试,使用模拟软件测试完毕后,完整代码以三个版本的形式进行介绍
1、版本一:使用串口接收数据超时完成一次数据的接收(STM32标准库)
2、版本二:进阶版-使用DMA形式进行数据发送和接收(STM32标准库)
3、版本三:初次使用HAL库完成对以上代码的修改工作(STM32HAL库)
modbus poll和modbus slave模拟软件下载(下载可直接使用)
modbus协议辅助软件下载-百度网盘链接 提取码:8g9c
modbus模拟软件使用方法-参考博客链接
modbus协议讲解及stm32实现—modbus协议看的一个视频,讲的很详细,软件也可以加群下载
b站up主上传了三个相关的视频
视频1
讲的很详细,可以细细的看一遍,讲解的内容包含了模拟软件的使用和部分代码的编写,看懂了写代码就方便很多----强烈推荐
视频2
总时长一个小时四十分钟,前面看了一个小时没有任何实质性的讲解(可以不看),一个小时才开始步入正题,看了几分钟,后面就没耐心看直接关了
视频3
第三个视频没有看,不清楚讲了啥
modbus协议
modbus协议基本知识不再进行赘述,详情点击以下链接
Modbus RTU 协议使用汇总-modbus协议参考博客链接1
MODBUS_RTU通讯规约–modbus协议参考博客链接2
1、主机可以读取从机的数据
2、从机修改数据后,主机这边会更新数据
3、主机对某个地址的值进行修改,从机那边对应更新
主机读取从机1个寄存器的数据
主机使用功能码03,读取从机地址01,起始地址为0x0001的一个寄存器数据
主机发送:01 03 00 01 00 01 D5 CA
最后两位数据是CRC校验位,发送数据时软件自动计算的数值追加到数据末尾发出去(软件右下角选择加校验ModbusCRC16,如果软件不支持需要使用在线的CRC校验计算之后手动添加校验位到数据末尾再发送)
主机读取从机2个寄存器的数据
主机向从机的1个寄存器中写入数据
主机向从机的多个寄存器中写入数据
01 10 00 05 00 02 04 01 02 03 04 92 9F
从00 05地址开始发送两个寄存器,四个字节的数据,数据分别发送的是01 02 03 04
定义一个结构体:(设备作为主机或从机时均会用到)
typedef struct
{
//作为从机时使用
u8 myadd; //本设备从机地址
u8 rcbuf[100]; //modbus接受缓冲区
u8 timout; //modbus数据持续时间
u8 recount; //modbus端口接收到的数据个数
u8 timrun; //modbus定时器是否计时标志
u8 reflag; //modbus一帧数据接受完成标志位
u8 sendbuf[100]; //modbus接发送缓冲区
//作为主机添加部分
u8 Host_Txbuf[8]; //modbus发送数组
u8 slave_add; //要匹配的从机设备地址(做主机实验时使用)
u8 Host_send_flag;//主机设备发送数据完毕标志位
int Host_Sendtime;//发送完一帧数据后时间计数
u8 Host_time_flag;//发送时间到标志位,=1表示到发送数据时间了
u8 Host_End;//接收数据后处理完毕
}MODBUS;
如果是HAL库中使用把u8换为uint8_t即可
首先看一下主机发送数据和从机发送数据的格式以及每一部分对应的含义
主机寻址从机时共发送8个字节的数据(从机地址-ID号1个字节,功能码1个字节,起始地址2个字节,寄存器个数2个字节,CRC校验位2个字节)
主机寻址从机读取寄存器数据还是比较好写的-就是填充一个8个字节的数组内容,然后通过串口将数据发出去
首先我们需要一个从机地址,然后是功能号(直接写死0X03),还有起始地址,读取的寄存器个数,CRC校验位(通过函数自主计算),那我们就只需要3个参数了
1-主机寻址从机读取寄存器数据(功能码0x03)
参数1从机地址(1个字节)–要寻址的从机
参数2起始地址(2个字节)–开始读取数据的地址
参数3寄存器个数(2个字节)–要读取的寄存器个数
//参数1从机地址,参数2起始地址,参数3寄存器个数
void Host_Read03_slave(uint8_t slave,uint16_t StartAddr,uint16_t num)
{
int j;
uint16_t crc;//计算的CRC校验位
modbus.slave_add=slave;//这是先把从机地址存储下来,后面接收数据处理时会用到
modbus.Host_Txbuf[0]=slave;//这是要匹配的从机地址
modbus.Host_Txbuf[1]=0x03;//功能码
modbus.Host_Txbuf[2]=StartAddr/256;//起始地址高位
modbus.Host_Txbuf[3]=StartAddr%256;//起始地址低位
modbus.Host_Txbuf[4]=num/256;//寄存器个数高位
modbus.Host_Txbuf[5]=num%256;//寄存器个数低位
crc=Modbus_CRC16(&modbus.Host_Txbuf[0],6); //获取CRC校验位
modbus.Host_Txbuf[6]=crc/256;//寄存器个数高位
modbus.Host_Txbuf[7]=crc%256;//寄存器个数低位
//发送数据包装完毕(共8个字节)
//开始发送数据
RS485_TX_ENABLE;//使能485控制端(启动发送)
for(j=0;j
2-主机对从机返回数据的处理:
(1)首先我们要判断一下数据是否接收完毕,只有在数据接收完毕的情况下我们才能对数据进行处理(modbus.reflag==1表示接收完毕),进入下一步。
(2)数据接收完毕之后我们需要将自行计算的CRC校验位和接收到的CRC校验位进行比较看其是否一致,如果一致的话才表明数据接收正确,进行下一步。
(3)判断完CRC校验位之后再去判断是不是主机寻址的从机返回的数据,如果符合条件进行下一步。
(4)以上条件都符合的条件下再去对接收到的数据做处理:从返回的数据中将有用的吧数据提取出来进行串口打印或者做其他用途
//主机接收从机的消息进行处理功能码0x03
void HOST_ModbusRX()
{
u16 crc,rccrc;//计算crc和接收到的crc
if(modbus.reflag == 0) //如果接收未完成则返回空
{
return;
}
//接收数据结束
//(数组中除了最后两位CRC校验位其余全算)
crc = Modbus_CRC16(&modbus.rcbuf[0],modbus.recount-2); //获取CRC校验位
rccrc = modbus.rcbuf[modbus.recount-2]*256+modbus.rcbuf[modbus.recount-1];//计算读取的CRC校验位
if(crc == rccrc) //CRC检验成功 开始分析包
{
if(modbus.rcbuf[0] == modbus.slave_add) // 检查地址是是对应从机发过来的
{
if(modbus.rcbuf[1]==3)//功能码时03
Host_Func3();//这是读取寄存器的有效数据位进行计算
}
}
modbus.recount = 0;//接收计数清零
modbus.reflag = 0; //接收标志清零
}
3-真正的数据处理函数void Host_Func3()
我们通过前面对从机返回数据的格式进行分析可知,数据中的第三个字节(对应modbus.rcbuf[2]位)是返回数据的有效个数,从第四个字节开始是数据的有效内容,一个寄存器数据分为高位和低位,所有两个字节是一个完整的数据,然后将其进行计算即可。
void Host_Func3()
{
int i;
int count=(int)modbus.rcbuf[2];//这是数据个数
printf("从机返回 %d 个寄存器数据:\r\n",count/2);
for(i=0;i
功能号0x06
1-主机发送:
对于要发送的数组填充工作如下:只不过这次增加了一个功能码参数
//向一个寄存器中写数据的参数设置
void Host_write06_slave(uint8_t slave,uint8_t fun,uint16_t StartAddr,uint16_t num)
{
uint16_t crc,j;//计算的CRC校验位
modbus.slave_add=slave;//从机地址赋值一下,后期有用
modbus.Host_Txbuf[0]=slave;//这是要匹配的从机地址
modbus.Host_Txbuf[1]=fun;//功能码
modbus.Host_Txbuf[2]=StartAddr/256;//起始地址高位
modbus.Host_Txbuf[3]=StartAddr%256;//起始地址低位
modbus.Host_Txbuf[4]=num/256;
modbus.Host_Txbuf[5]=num%256;
crc=Modbus_CRC16(&modbus.Host_Txbuf[0],6); //获取CRC校验位
modbus.Host_Txbuf[6]=crc/256;//寄存器个数高位
modbus.Host_Txbuf[7]=crc%256;//寄存器个数低位
//发送数据包装完毕
//开始发送数据
RS485_TX_ENABLE;//使能485控制端(启动发送)
for(j=0;j
2-主机接收的从机数据处理:
从机地址+功能码+起始地址+成功写入的寄存器个数+CRC
只是辅助一下从机已经根据指令往对应的寄存器中写入了数据
//从机返回数据
void Host_Func6()
{
int crc,rccrc;
crc = Modbus_CRC16(&modbus.rcbuf[0],6); //获取CRC校验位
rccrc = modbus.rcbuf[6]*256+modbus.rcbuf[7];//计算读取的CRC校验位
if(crc == rccrc) //CRC检验成功 开始分析包
{
if(modbus.rcbuf[0] == modbus.slave_add) // 检查地址是是对应从机发过来的
{
if(modbus.rcbuf[1]==6)//功能码时06
{
printf("地址为 %d 的从机寄存器 %d 中写入数据 %d \r\n ",(int)modbus.rcbuf[0],(int)modbus.rcbuf[3]+((int)modbus.rcbuf[2])*256,(int)modbus.rcbuf[5]+((int)modbus.rcbuf[4])*256);
printf("Host_06 write data right!\r\n");
}
}
}
}
功能码0x10
主机发送数据的数组填充内容:
只需要根据上面将缺少的内容再自主增加至参数按照顺序依次对数组进行填充数据即可
从机地址+功能码+起始地址+寄存器个数+写入字节数+写入的具体数据+CRC校验
从机返回数据处理:
根据上面的将0x06功能码修改为0x10即可
设备作为从机使用时肯定有自己专们的一个地址和相关的寄存器,先定义一个寄存器:主机读数据和写数据时操作的寄存器
u16 Reg[] ={0x0001,
0x0012,
0x0013,
0x0004,
0x0025,
0x0036,
0x0007,
0X0008,
};//reg是提前定义好的寄存器和寄存器数据,要读取和改写的部分内容
1-本设备作为从机时的地址
// Modbus初始化函数
void Modbus_Init()
{
modbus.myadd = 0x02; //从机设备地址为2
modbus.timrun = 0; //modbus定时器停止计算
modbus.slave_add=0x01;//主机要匹配的从机地址(本设备作为主机时)
}
2-只有当数据接收完毕时才进行数据处理
(1)首先判断自主计算的CRC校验位和接收到数据的校验位是否一致
(2)其次判断从机地址是不是自己的地址
(3)数据传输正确且从机地址正确的情况下再根据不同的功能码去执行对应的函数操作
0x03读取寄存器数据
0x06写入一个寄存器数据
0x10写入多个寄存器数据
3-事件处理总体函数
// Modbus事件处理函数
void Modbus_Event()
{
u16 crc,rccrc;//crc和接收到的crc
//没有收到数据包
if(modbus.reflag == 0) //如果接收未完成则返回空
{
return;
}
//收到数据包(接收完成)
//通过读到的数据帧计算CRC
//参数1是数组首地址,参数2是要计算的长度(除了CRC校验位其余全算)
crc = Modbus_CRC16(&modbus.rcbuf[0],modbus.recount-2); //获取CRC校验位
// 读取数据帧的CRC
rccrc = modbus.rcbuf[modbus.recount-2]*256+modbus.rcbuf[modbus.recount-1];//计算读取的CRC校验位
//等价于下面这条语句
//rccrc=modbus.rcbuf[modbus.recount-1]|(((u16)modbus.rcbuf[modbus.recount-2])<<8);//获取接收到的CRC
if(crc == rccrc) //CRC检验成功 开始分析包
{
if(modbus.rcbuf[0] == modbus.myadd) // 检查地址是否时自己的地址
{
switch(modbus.rcbuf[1]) //分析modbus功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbus_Func3(); break;//这是读取寄存器的数据
case 4: break;
case 5: break;
case 6: Modbus_Func6(); break;//这是写入单个寄存器数据
case 7: break;
case 8: break;
case 9: break;
case 16: Modbus_Func16(); break;//写入多个寄存器数据
}
}
else if(modbus.rcbuf[0] == 0) //广播地址不予回应
{
}
}
modbus.recount = 0;//接收计数清零
modbus.reflag = 0; //接收标志清零
}
1-作为从机时返回的数据内容:对数组进行填充
第一个字节必然是从机地址
第二个字节是功能码
第三个字节是我要给主机返回几个字节的数据
第四个字节开始对应寄存器的具体内容(每个寄存器占2个字节)
第n个字节具体数据内容结束
对前面所有字节进行CRC校验计算并将CRC计算的数据追加到数组的结尾
数据封装完毕之后将封装好的数组数据发送出去
/*
********************************************************************************
主机:03
01 03 00 01 00 01 D5 CA 从地址01开始读读取一个寄存器的数据内容
ID 功能码 起始地址 读取寄存器的个数
从机返回:
01 03 02 00 03 F8 45 返回了两个字节的数据,数据是00 03
ID 功能码 几个字节 返回的数据内容
********************************************************************************
*/
// Modbus 3号功能码函数
// Modbus 主机读取寄存器值
void Modbus_Func3()
{
u16 Regadd,Reglen,crc;
u8 i,j;
//得到要读取寄存器的首地址
Regadd = modbus.rcbuf[2]*256+modbus.rcbuf[3];//读取的首地址
//得到要读取寄存器的数据长度
Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
//发送回应数据包
i = 0;
modbus.sendbuf[i++] = modbus.myadd; //ID号:发送本机设备地址
modbus.sendbuf[i++] = 0x03; //发送功能码
modbus.sendbuf[i++] = ((Reglen*2)%256); //返回字节个数
for(j=0;j
这是从机接收从机的指令往1个寄存器中写入数据
第3-4个字节是要写入的地址
第5-6个字节是要写入的数据
从机返回数组填充内容:将接收到的数据原路返回即可
// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()
{
u16 Regadd;//地址16位
u16 val;//值
u16 i,crc,j;
i=0;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要修改的地址
val=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //修改后的值(要写入的数据)
Reg[Regadd]=val; //修改本设备相应的寄存器
//以下为回应主机
modbus.sendbuf[i++]=modbus.myadd;//本设备地址
modbus.sendbuf[i++]=0x06; //功能码
modbus.sendbuf[i++]=Regadd/256;//写入的地址
modbus.sendbuf[i++]=Regadd%256;
modbus.sendbuf[i++]=val/256;//写入的数值
modbus.sendbuf[i++]=val%256;
crc=Modbus_CRC16(modbus.sendbuf,i);//获取crc校验位
modbus.sendbuf[i++]=crc/256; //crc校验位加入包中
modbus.sendbuf[i++]=crc%256;
//数据发送包打包完毕
RS485_TX_ENABLE;;//使能485控制端(启动发送)
for(j=0;j
向多个寄存器中写入数据
按照主机的指令往设备的寄存器地址中写入数据
第1个字节是本设备地址(从机)
第2个字节是功能码0X06
第3、4个字节是写入数据的起始地址
第5、6个字节是写入的寄存器个数
第7个字节是写入的字节个数(字节个数=寄存器个数*2)
第8个字节开始是要写入的数据
从机要返回的数据:
只需要把前6个字节装入数组,再对这6个字节进行CRC校验,将计算的数值追加到数组末尾再发送出去即可
//这是往多个寄存器器中写入数据
//功能码0x10指令即十进制16
void Modbus_Func16()
{
u16 Regadd;//地址16位
u16 Reglen;
u16 i,crc,j;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //要修改内容的起始地址
Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
for(i=0;i
以上所有部分便是MODBUS RTU协议代码的编写部分,接下来便是其他代码部分的编写了
Modbus协议是建立在485通信基础上的,RS485通信与普通串口通信的本质区别就是多了一个发送数据和接收数据控制位。
整个测试中使用串口1进行打印调试
使用串口2作为RS485通信时的收发数据
1、串口
STM32串口学习部分的博客—参考链接
串口2初始化部分
(直接复制原子哥的串口1代码改为串口2)
//485串口初始化
//初始化IO 串口2
//bound:波特率
void Modbus_uart2_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;//GPIO结构体指针
USART_InitTypeDef USART_InitStructure;//串口结构体指针
NVIC_InitTypeDef NVIC_InitStructure;//中断分组结构体指针
//1、使能串口时钟,串口引脚时钟 串口2挂载到APB1上
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); //使能USART2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD,ENABLE);//使能串口时钟和收发使能时钟
//2、复位串口
USART_DeInit(USART2); //复位串口1
//3、发送接收引脚的设置
//USART2_TX PA.2(由图 可知设置为推挽复用输出)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA9
//USART2_RX PA.3(有图可知浮空输入)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA10
//485收发控制引脚PD7
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //普通的推挽输出
GPIO_Init(GPIOD, &GPIO_InitStructure); //初始化PA9
//4、USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART2, &USART_InitStructure); //初始化串口
//5、Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//6、开启接收数据中断
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中断
//7、使能串口
USART_Cmd(USART2, ENABLE); //使能串口
RS485_RX_ENABLE;//使能接收引脚(常态下处于接收状态)
}
定义一个串口2发送字节的函数
//modbus串口发送一个字节数据
void Modbus_Send_Byte(u8 Modbus_byte)
{
USART_SendData(USART2,Modbus_byte);
while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
USART_ClearFlag(USART2, USART_FLAG_TC);
}
如果不使用RS485通信的情况下直接调用这个函数进行发送数据即可,由于此程序是建立在485通信的基础之上,所以在发送数据前需要将将485发送数据控制引脚使能,发送数据完毕之后需要再切换回数据接收模式
485通信
第一行代码便是启动485通信发送模式,最户一行便是关闭发送,启动接收模式
RS485_TX_ENABLE;;//使能485控制端(启动发送)
for(j=0;j
通过以上代码便可以将数组中的数据发送出去了,其中i是要发送的字节个数
串口中断函数
主要是将接收到的数据依次存放到对应的数组中
当modbus.reflag==1表示还有数据正在处理中,反之则进行数据存储,当开始存储第二个数据时开启定时器计时,主要目的判断接收数据是否完毕,如果超过一段时间没有数据,则表明这一次数据接收完毕
//modbus串口中断服务程序
void USART2_IRQHandler(void)
{
u8 st,Res;
st = USART_GetITStatus(USART2, USART_IT_RXNE);
if(st == SET)//接收中断
{
Res =USART_ReceiveData(USART2); //读取接收到的数据
// USART_SendData(USART1, Res);//接受到数据之后返回给串口1
if( modbus.reflag==1) //有数据包正在处理
{
return ;
}
modbus.rcbuf[modbus.recount++] = Res;
modbus.timout = 0;
if(modbus.recount == 1) //已经收到了第二个字符数据
{
modbus.timrun = 1; //开启modbus定时器计时
}
}
}
2、定时器
定时器中断函数
定时器设置1ms进入中断1次,运行时间不为0的情况下开始计时,超过8ms则表明这一次接收数据完毕,将数据接收结束标志位置1处理(modbus.reflag = 1),当数据接收完毕,则STM32可以对接收到的数据进行数据分析和处理执行相应的操作了
下面的变量主要是实现1s计时操作
// Modbus 定时器中断函数 1ms中断一次
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIMx的中断待处理位:TIM 中断源
if(modbus.timrun != 0)//运行时间!=0表明
{
modbus.timout++;
if(modbus.timout >=8)
{
modbus.timrun = 0;
modbus.reflag = 1;//接收数据完毕
}
}
modbus.Host_Sendtime++;//发送完上一帧后的时间计数
if(modbus.Host_Sendtime>1000)//距离发送上一帧数据1s了
{
//1s时间到
modbus.Host_time_flag=1;//发送数据标志位置1
}
}
}
3、main.c部分
把主机部分和从机使用部分进行了整合(额外加入了按键和LED作为辅助)
主要实现功能:
(1)无按键按下的情况:
没有任何按键按下的情况下是主机模式,此时主机去寻址从机地址为01的设备获取数据
(2)有按键按下的情况下:
通过按键寻址不同的从机
按键1查看从机01的数据
按键2查看从机02的数据
按键3查看从机03的数据
按键4表明由主机切换到从机模式(此设备作为从机地址0x02)
变量定义:
//加入的按键切换主机模式为从机模式
int slave=0;//从机id
int host_slave_flag=0;//0-默认情况下本设备是主机,1-本设备切换为从机模式
uint8_t key_value=0;//哪一个按键按下了1-4
uint8_t key_flag=0;//key_flag等于0表示从来没有按键按下(此时一直查看从机1的数据)-----如果不添加此标志,下载程序后需要复位操作
按键部分
//按键1查看从机01的数据
//按键2查看从机02的数据
//按键3查看从机03的数据
//按键4由主机切换到从机模式(此设备作为从机地址0x02)
void key_Send()
{
key_value=KEY_Scan(1);
switch(key_value)
{
case 1:
slave=1;key_flag=1;break;//从机地址01
case 2:
slave=2;key_flag=1;break;//从机地址02
case 3:
slave=3;key_flag=1;break;//从机地址03
case 4:
host_slave_flag=1;key_flag=1;break;//切换为从机模式
}
}
主函数部分
int main(void)
{
delay_init(); //延时函数初始化
NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(9600); //串口1初始化为9600(只用来打印测试数据)
LED_Init(); //LED端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
Modbus_uart2_init(4800);//初始化modbus串口2和485控制引脚
Modbus_TIME3_Init(7200-1,10-1);//定时器初始化参数1是重装载数,参数2是分频系数//1ms中断一次
Modbus_Init();//MODBUS初始化--本机做作为从机设备地址,本机要匹配的从机地址
while(1)
{
key_Send();//按键扫描
if(host_slave_flag==1)//这是按键4按下了,表明是从机模式(led4不停的闪烁)
{
Modbus_Event();//Modbus事件处理函数(执行读或者写的判断)--从机地址01
if(Reg[3]==0x0A)//作为从机如果寄存器的地址00 03收到了0x0A数据则打开LED3
{
LED1=0;
}
if(Reg[3]==0x0B)
{
LED1=1;
}
LED4=~LED4;
delay_ms(100);
}
else if(key_flag==0)//表示开机后没有按键按下(主机模式查看从机地址01的数据)
{
//参数1:查看第i个从机数据
Host_Read03_slave(0x01,0x0000,0x0001);//参数2起始地址,参数3寄存器个数
if(modbus.Host_send_flag)
{
modbus.Host_Sendtime=0;//发送完毕后计数清零(距离上次的时间)
modbus.Host_time_flag=0;//发送数据标志位清零
modbus.Host_send_flag=0;//清空发送结束数据标志位
HOST_ModbusRX();//接收数据进行处理
}
LED2=~LED2;
delay_ms(1000);
}
else
{
if(modbus.Host_time_flag)//每1s发送一次数据
{
//参数1:查看第i个从机数据
Host_Read03_slave(slave,0x0000,0x0003);//,参数2起始地址,参数3寄存器个数
if(modbus.Host_send_flag)
{
modbus.Host_Sendtime=0;//发送完毕后计数清零(距离上次的时间)
modbus.Host_time_flag=0;//发送数据标志位清零
modbus.Host_send_flag=0;//清空发送结束数据标志位
HOST_ModbusRX();//接收数据进行处理
}
LED3=~LED3;
}
}
}
}
测试:
按下按键4之后使用modbus poll作为主机链接之后如图
写操作
代码下载
模式1代码下载链接-STM32+RS485+MODBUS协议(主机+从机代码)+串口+定时器
使用DMA数据进行传输
DMA部分的使用参考链接:对DMA的使用很有帮助
STM32-DMA数据传输(USART-ADC-数组)–链接1
STM32-ADC(独立模式、双重模式)+DMA读取数据+部分基础知识–链接2
DMA数据传输有常规模式和循环模式两种,此处使用的是常规模式,由于常规模式下只能发送一次数据,只发送一次数据肯定是不可以的,因为我们要多次发送数据,因此要先解决这个问题,实现可以多次发送数据。
在使用之前肯定需要先在串口发送数据的基础上进行DMA发送数据改造最终实现多次发送数据,具体理解参考下面博主的博客进行学习,下面直接上代码。
STM32 DMA正常模式等待传输完成和开始下一次传输–链接
重新使能DMA
在DMA每次发送数据之前都需要调用此函数进行重新使能
主机寻址从机时发送的字节个数永远是8个字节数据
//这是DMA重装传输数目并使能
void DMA_TX_Enable()
{
//重新装入要发送的字符数并使能
DMA_Cmd (DMA1_Channel7,DISABLE);//关闭DMA通道
DMA_ClearFlag(DMA1_FLAG_TC7);//清标志
DMA_SetCurrDataCounter(DMA1_Channel7,8);//重置传输数目
DMA_Cmd (DMA1_Channel7,ENABLE);//开启DMA通道
}
既然发送数据需要重新使能,那我们接收数据也需要重新使能,不可能只接收一次数据,主机寻址从机时,从机返回的数据字节个数=固定的5个字节+寄存器个数*2
//这是DMA重装传输数目并使能(dmA接收)
void DMA_RX_Enable(uint8_t num)//num寄存器的个数
{
DMA_Cmd(DMA1_Channel6,DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC6);//先将这个接收标志位清除
DMA_SetCurrDataCounter(DMA1_Channel6, num*2+5); //这是从机返回的字符个数:read_num*2+5
DMA_Cmd(DMA1_Channel6, ENABLE);
}
DMA发送数据函数
//DMA发送数据函数
void DMA_TX_data()
{
//发送数据
RS485_TX_ENABLE;//使能485控制端(启动发送)
while(DMA_GetFlagStatus(DMA1_FLAG_TC7)==RESET);//如果返回值位reset表示还没哟传输成功//等待发送完毕
delay_ms(5);//如果不加这个延时将丢失最后两个字节数据
RS485_RX_ENABLE;//开启接收
}
主机填充发送的数据+对从机返回的数据进行处理
//寻址从机发送指令并对接收的数据处理函数
//参数1从机地址,参数2起始地址,参数3寄存器个数
void read03(uint8_t slave,uint16_t StartAddr,uint16_t num)
{
//发送数据
Host_read03_set(slave,StartAddr,num);
//接收的数据处理
if(modbus.Host_End!=1)
{
HOST_ModbusRX();//进行数据处理
}
}
函数整合
对上面函数的调用进行整合使用,在主函数调用此函数即可实现数据的发送和接收数据的处理
//读取从机的寄存器数据参数设置并发送数据
void Host_read03_set(uint8_t slave,uint16_t StartAddr,uint16_t num)
{
//发送重新使能
DMA_TX_Enable();//发送重新使能
Host_Read03_slave(slave,StartAddr,num);//填充发送数据的数组内容
modbus.Host_End=0;//数据处理完成标志位清零
DMA_TX_data();//发送数据
//接收重新使能
DMA_RX_Enable(num);//重装接收数据个数--num寄存器的个数
}
主机向从机的1个寄存器写入数据函数
//参数设置+数据发送
void Host_write06_set(uint8_t slave,uint8_t fun,uint16_t StartAddr,uint16_t num)
{
//发送重新使能
DMA_TX_Enable();//发送重新使能
Host_write06_slave(slave,fun,StartAddr,num);
modbus.Host_End=0;
//发送数据
DMA_TX_data();
//接收重新使能
DMA_RX_Enable(num);//重装接收数据个数--num寄存器的个数
}
DMA中断函数
(只开启DMA的接收中断即可,串口2除了基本的初始化配置不需要开启中断,不需要编写中断服务函数)
//DMA接收中断
void DMA1_Channel6_IRQHandler()
{
//这是数据读取完毕
DMA_ClearITPendingBit(DMA1_IT_TC6);//清楚传送完毕标志位
modbus.reflag=1;//表明接收数据完毕
}
main.c文件的内容
功能和使用介绍如下:
/*
串口1打印数据9600
串口2是485Modbus通信4800
主要实现作为主机时功能码0x03和0x06的使用
函数1实现读取从机的寄存器数据
//参数1从机地址,参数2起始地址,参数3寄存器个数
void read03(uint8_t slave,uint16_t StartAddr,uint16_t num);
函数2:往从机的1个寄存器中写入数据
参数1从机地址,参数2功能码,参数3写入的地址,参数4写入的数据
void Host_write06_set(uint8_t slave,uint8_t fun,uint16_t StartAddr,uint16_t num)
主循环中默认是功能码0x03的代码测试部分
下面被注释掉的是0x06写入一个寄存器数据
*/
宏定义设置
//宏定义设置区
#define sl_ID 0x01 //从机地址
#define st_address 0x0000 //起始地址
#define sl_num 3 //读取寄存器个数
#define slave_count 3 //要读取的从机个数
主函数内容
int main(void)
{
int i=sl_ID ; //第一个从机地址
delay_init(); //延时函数初始化
NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(9600); //串口1初始化为9600(只用来打印测试数据)
LED_Init(); //LED端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
USART2_DMA_TX_config();//DMA发送初始化
USART2_DMA_RX_config();//DMA接收初始化
Modbus_uart2_init(4800);//初始化modbus串口2和485控制引脚
Modbus_TIME3_Init(7200-1,10-1);//定时器初始化参数1是重装载数,参数2是分频系数//1ms中断一次
Modbus_Init();//MODBUS初始化--本机做作为从机设备地址,本机要匹配的从机地址
USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE);//开启使能
USART_DMACmd(USART2,USART_DMAReq_Rx,ENABLE);//开启使能
modbus.Host_End=1;
while(1)
{
if(modbus.Host_time_flag)//1s时间到标志位
{
modbus.Host_time_flag=0;
modbus.Host_Sendtime=0;//计时标志位清零
//从机地址,起始地址,寄存器个数
read03(i,st_address,sl_num);//数据处理
if(modbus.Host_End)//数据处理完毕标志位
{
i++;//从机地址+1
if(i>slave_count)//从机的个数判断
i=1;//第一个从机地址
}
}
往寄存器中写入数据测试部分,可打开注释测试写入数据
//发送数据
// Host_write06_set(0x01,0x06,0x0003,0x0089);//往从机01的寄存器地址0x0003中写入0x0089(十进制137)
// //接收的数据处理
// if(modbus.Host_End!=1)
// {
// Host_Func6();//进行数据处理
// }
// delay_ms(1000);
// LED0=~LED0;
}
}
测试:
代码下载
模式2代码下载链接-STM32+RS485+DMA+modbus协议
初始HAL库
这次HAL库是个意外,也许是个新的开始。老师以为我一直在学HAL库的使用,其实我一直在看STM32标准库,以至于我把写好的代码发给老师后,第二天老师问我咋配置的,我才知道我俩整差了,然后利用一天时间在标准库的基础上把代码改成了HAL库(直接复制标准库下的modbus文件夹过去稍作修改,只用stm32cube进行定时器,串口、RS485部分的配置即可)
才开始打算直接用HAL库改DMA形式传输数据,改了好多次没成功就放弃,直接用串口超时接收的形式进行数据收发了,经历过DMA后这次认怂比较快,用串口肯定还是不能直接干,当然要从点灯开始,首先学着使用STM32cube生成代码去点灯,做按键实验,然后有点感觉了才开始搞点大动作,串口1,串口2,定时器,RS485控制引脚齐上阵。
由于第一次使用HAL库着实踩了不少雷,和标准库开发还是有不少差距的,HAL库是主流,也许该学HAL库了。
使用过程中主要参考以下博客
1-STM32CubeMX教程–功能介绍
2-STM32串口接收中断——基于HAL库
3-STM32 HAL库 CubeMX教程(二)定时器基本使用
主要用到的部分
在生成代码的过程中主要用到了RS485发送/接收数据的使能引脚、串口1(用于打印调试信息)、串口2(用于485通信)、定时器1用于定时计数
485使能引脚
RCC设置
sys设置如下:
串口1
串口2
定时器
NVIC部分
时钟配置:
生成代码
串口1重定向
首先对串口1部分进行代码改造-重定向printf
STM32串口重定向printf出现FILE未定义问题—参考链接1
STM32 printf 死机 printf半主机模式–参考链接2
在uart.c文件中添加以下代码,为了可以直接使用printf打印调试信息------别忘了调用stdio.h头文件
/* USER CODE BEGIN 0 */
//串口1重定向
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xfff);
return ch;
}
#endif
/* USER CODE END 0 */
串口2中断服务函数中加入以下代码
/* USER CODE BEGIN USART2_IRQn 1 */
// HAL_UART_Transmit(&huart1,&RES,1,100); // 接收到数据马上使用串口1发送出去
if( modbus.reflag==1) //有数据包正在处理
{
return ;
}
modbus.rcbuf[modbus.recount++] = RES;
modbus.timout = 0;
if(modbus.recount == 1) //已经收到了第二个字符数据
{
modbus.timrun = 1; //开启modbus定时器计时
}
HAL_UART_Receive_IT(&huart2, (uint8_t *)&RES,1); //添加的一行代码
/* USER CODE END USART2_IRQn 1 */
MODBUS文件
modbus.c文件和modbus.h文件直接复制过来添加上即可,该调用头文件的地方调用对应的头文件即可
也要对其进行替换
u16替换为uint16_t
u8替换为uint8_t
在main.h中加入以下代码
/* USER CODE BEGIN Private defines */
//485发送接收控制引脚
//接收使能
#define RS485_RX_ENABLE HAL_GPIO_WritePin(RS485_control_GPIO_Port,RS485_control_Pin,GPIO_PIN_RESET)
//发送使能
#define RS485_TX_ENABLE HAL_GPIO_WritePin(RS485_control_GPIO_Port,RS485_control_Pin,GPIO_PIN_SET)
void Modbus_Send_Byte( uint8_t ch );//发送字符函数
extern uint8_t RES;//串口接收缓冲区
/* USER CODE END Private defines */
在main.c中添加以下代码
/* USER CODE BEGIN PTD */
uint8_t RES;
/***************** 发送一个字符 **********************/
//使用单字节数据发送前要使能发送引脚,发送后要使能接收引脚。
void Modbus_Send_Byte( uint8_t ch )
{
/* 发送一个字节数据到USART2 */
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xff);
}
/* USER CODE END PTD */
在主函数下方添加定时器部分的代码
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if (htim->Instance == htim1.Instance)
{
if(modbus.timrun != 0)//运行时间!=0表明
{
modbus.timout++;
if(modbus.timout >=8)
{
modbus.timrun = 0;
modbus.reflag = 1;//接收数据完毕
}
}
modbus.Host_Sendtime++;//发送完上一帧后的时间计数
if(modbus.Host_Sendtime>1000)//距离发送上一帧数据1s了
{
//1s时间到
modbus.Host_time_flag=1;//发送数据标志位置1
}
}
}
/* USER CODE END 4 */
主函数的初始化部分加入以下代码
/* USER CODE BEGIN 2 */
Modbus_Init();//本机作为从机使用时初始化
HAL_TIM_Base_Start_IT(&htim1);
printf("RS485_TEST_01\r\n");
RS485_RX_ENABLE;
HAL_UART_Receive_IT(&huart2, (uint8_t *)&RES, 1);//调用接收中断函数
/* USER CODE END 2 */
while(1)主要代码
共包含3部分测试(每一部分需单独测试)
1-主机读取从机数据测试(已经打开注释)
2-主机向从机的一个寄存器中写入数据
3-本设备作为从机使用,作为从机时地址为0x02,测试完一个注释掉,再打开另一个测试
/* USER CODE BEGIN WHILE */
while (1)
{
if(modbus.Host_time_flag)//每1s发送一次数据
{
//01-读取从机数据测试
//参数1:查看第i个从机数据
Host_Read03_slave(0x01,0x0000,0x0003);//参数1从机地址,参数2起始地址,参数3寄存器个数
if(modbus.Host_send_flag)
{
modbus.Host_Sendtime=0;//发送完毕后计数清零(距离上次的时间)
modbus.Host_time_flag=0;//发送数据标志位清零
modbus.Host_send_flag=0;//清空发送结束数据标志位
HOST_ModbusRX();//接收数据进行处理
}
// //02-写入数据测试
// Host_write06_slave(0x01,0x06,0x0001,0x0045);
// if(modbus.Host_send_flag)
// {
// modbus.Host_Sendtime=0;//发送完毕后计数清零(距离上次的时间)
// modbus.Host_time_flag=0;//发送数据标志位清零
// modbus.Host_send_flag=0;//清空发送结束数据标志位
// Host_Func6();//从机返回数据处理
// }
//
//
// //3-作为从机使用
// Modbus_Event();//本机作为从机使用时
}
// HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
STM32标准库和HAL库均介绍完毕。
代码下载
HAL库代码下载链接–STM32HAL库+RS485+串口+定时器+Modbus协议(主机+从机测试)
最后说下使用HAL库踩的雷:
1- 代码不放在指定位置丢失
刚开始不知道自己写的代码必须写到指定区域,如果不是用户区域的位置,重新生成代码加载的时候之前写的代码就白写了,真实消失的一干二净。—代码丢失了2次才发觉这个问题
2- 仿真器出故障板子无法下载
使用的仿真器是免驱动类型,下载了某个程序后确提示检测不到仿真器,无法下载程序,换板子仍然是同样的情况,-尝试了百度上很多方法都不行,就差改下载模式了(没敢试)
这里真的浪费了不少时间,得认怂了,仿真器不行还有串口下载,然后使用串口下载程序,发现可以,就又尝试仿真器下载,居然可以用了–后来也找到了正解。
参考链接1
参考链接2
避免出现这种错误:切记不要忘记选择debug方式
解决问题最直接方式:使用串口下载程序即可解决此问题
3、使用STM32cube生成代码错误
参考链接3-解决办法
4、文件保存失败–真是个头大的问题
对于重新生成的代码需要加载一下,大多数情况点“是”选项代码就更新了,也有好多次出现头文件需要重新保存,Ctrl+S的时候会提示保存的文件名和保存路径,如果在新的路径下保存,即使添加上路径,会报错(因为在默认的路径下还有一个此文件),保存在默认的路径下替换文件,也会出现错误。
5、过完年HAL库开发估计继续踩雷