因为项目需要,需要使用倍福控制器控制舵机,但是舵机是5v,gnd,信号线,所以想到了自己绘制单片机板子,单片机板子在我上一篇博客里面讲过了制作过程,包括,绘制原理图,接线铺铜等等,这一章讲,我是怎么通过plc的485通讯控制舵机摆动的。
一、总体思路
单片机:在这个项目中,单片机作为下位机(动作执行者),将动作写死在状态里。
西门子1214C:1214C作为上位机(选择动作),给单片机执行动作的指令。
通讯:由于485有较强的抗干扰能力,选择使用RS485,作为两者的通讯。
二、单片机代码
由于PLC通过485发送的数据是报文形式的,例如:06 03 00 01 00 01 __ __这种数组,所以单片机是通过485来接收到这些数组,并返回数组,表示PLC与单片机通讯成功。因此,需要串口接收中断,来接收这种数组,并对数组的数据位进行分类,并在主函数中执行对应类别的动作。

在
cpp
#include "rs485.h"
#include "delay.h"
#include "usart.h"
#include "TIM.h"
uint8_t RS485_RX_BUF[64];
uint8_t RS485_RX_CNT = 0;
uint16_t Holding_Reg[100]; // 对应40001~40100
extern volatile u8 flag; // 定义接收完成标志
extern volatile uint8_t state;
#define SLAVE_ADDR 0x06 // 与PLC设定的地址一致
//------------------------------------------------------------------
// 初始化
//------------------------------------------------------------------
void RS485_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
// TX PA2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// RX PA3
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// RS485 DE/RE PA4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
RS485_TX_EN = 0; // 默认接收模式
// 串口参数
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
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);
USART_Cmd(USART2, ENABLE);
// NVIC中断配置
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 开启接收中断
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
}
//------------------------------------------------------------------
// USART2中断:接收Modbus请求帧
//------------------------------------------------------------------
void USART2_IRQHandler(void)
{
static uint8_t rx_state = 0;
static uint8_t rx_len = 0;
if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
{
uint8_t res = USART_ReceiveData(USART2);
//LCD_ShowNum(100, 200, 1, 2, 24);
if (rx_state == 0)
{
if (res == SLAVE_ADDR) // 从机地址正确
{
RS485_RX_BUF[0] = res;
rx_len = 1;
rx_state = 1;
}
//LCD_ShowNum(100, 200, RS485_RX_BUF[0], 2, 24);
}
else
{
RS485_RX_BUF[rx_len++] = res;
// 典型Modbus主机读命令固定8字节
if (rx_len >= 8)
{
if(RS485_RX_BUF[1] == 0x03)
{
rx_state = 0;
rx_len = 0;
Modbus_Process();
//LCD_ShowNum(100, 200, 9, 2, 24);
}
if(RS485_RX_BUF[1] == 0x06)
{
rx_state = 0;
rx_len = 0;
Modbus_Process();
//LCD_ShowNum(100, 200, 9, 2, 24);
}
}
}
//USART_ClearITPendingBit(USART2, USART_IT_RXNE);
}
}
//------------------------------------------------------------------
// RS485发送函数
//------------------------------------------------------------------
void RS485_Send_Data(uint8_t *buf, uint8_t len)
{
RS485_TX_EN = 1; // 发送模式
for (uint8_t i = 0; i < len; i++)
{
USART_SendData(USART2, buf[i]);
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
}
while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
//Delay_us(200); // 延时确保最后字节发出
RS485_TX_EN = 0; // 回接收模式
}
uint16_t Modbus_CRC16(uint8_t *buf, uint8_t len)
{
uint16_t crc = 0xFFFF;
for (uint8_t i = 0; i < len; i++)
{
crc ^= buf[i];
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
void Modbus_Process(void)
{
uint8_t slave_id = RS485_RX_BUF[0];
uint8_t func = RS485_RX_BUF[1];
if(func == 0x03)
{
uint16_t start_addr = (RS485_RX_BUF[2] << 8) | RS485_RX_BUF[3];
uint16_t reg_count = (RS485_RX_BUF[4] << 8) | RS485_RX_BUF[5];
uint16_t crc_recv = RS485_RX_BUF[6] | (RS485_RX_BUF[7] << 8);
uint16_t crc_calc = Modbus_CRC16(RS485_RX_BUF, 6);
// 校验
if (slave_id != SLAVE_ADDR) return;
if (func != 0x03) return;
if (crc_recv != crc_calc) return;
if (start_addr + reg_count > 100) return;
uint8_t txbuf[128];
txbuf[0] = slave_id;
txbuf[1] = 0x03;
txbuf[2] = reg_count * 2;
for (uint16_t i = 0; i < reg_count; i++)
{
uint16_t val = Holding_Reg[start_addr + i];
txbuf[3 + 2 * i] = val >> 8;
txbuf[4 + 2 * i] = val & 0xFF;
}
uint16_t crc = Modbus_CRC16(txbuf, 3 + reg_count * 2);
txbuf[3 + reg_count * 2] = crc & 0xFF; // CRC低字节
txbuf[4 + reg_count * 2] = crc >> 8; // CRC高字节
RS485_Send_Data(txbuf, 5 + reg_count * 2);
}
if(func == 0x06)
{
uint16_t reg_addr = (RS485_RX_BUF[2] << 8) | RS485_RX_BUF[3];
uint16_t reg_value = (RS485_RX_BUF[4] << 8) | RS485_RX_BUF[5];
uint16_t crc_recv = (RS485_RX_BUF[6] << 8) | RS485_RX_BUF[7];
uint16_t crc_calc = Modbus_CRC16(RS485_RX_BUF, 6);
if (slave_id != SLAVE_ADDR) return;
if (func != 0x06) return;
//if (crc_recv != crc_calc) return;//过滤杂波
if (reg_addr >= 100) return;
// 写入寄存器
Holding_Reg[9] = reg_value;
if(Holding_Reg[9] == 0x01)
{
flag = 1;
}
if(Holding_Reg[9] == 0x02)
{
flag = 2;
}
if(Holding_Reg[9] == 0x03)
{
flag = 3;
}
// 回复帧(与请求帧完全相同)
uint8_t txbuf[8];
txbuf[0] = slave_id;
txbuf[1] = 0x06;
txbuf[2] = reg_addr >> 8;
txbuf[3] = reg_addr & 0xFF;
txbuf[4] = reg_value >> 8;
txbuf[5] = reg_value & 0xFF;
uint16_t crc = Modbus_CRC16(txbuf, 6);
txbuf[6] = crc & 0xFF; // CRC低字节
txbuf[7] = crc >> 8; // CRC高字节
RS485_Send_Data(txbuf, 8);
}
}
我在接收到数据后,对发送过来的数据进行判断并赋予状态:
功能码为:03为读取寄存器,06为写入寄存器
数据位为:0x01为停止状态位"flag = 1",0x02为慢速摆动状态位"flag = 2",0x03为快速摆动状态位"flag = 3",在主函数中,写入每一个状态位对应的动作指令即可:
cpp
#include "stm32f10x.h" // Device header
#include "PWM.h"
#include "Delay.h"
#include "rs485.h"
#include "TIM.h"
#include "init.h"
volatile u8 flag = 1;
extern uint16_t Holding_Reg[100];
volatile uint8_t state = 0;
#define SLAVE_ADDR 0x06 // 与PLC设定的地址一致
int main()
{
rcc_init();
// int state = 0;
u8 i;
Encoder_Init();
RS485_Init();
PWM_Init();
//flag = 2;
// state = 5;
//通信调试//
// while(1)
// {
// u8 n[2] = {0x01, 0x02};
// uint8_t p0[] = {0x00};
// uint8_t p1[] = {0xFF};
// uint8_t p2[] = {0x55}; // 01010101
// uint8_t p3[] = {0xAA}; // 10101010
// RS485_Send_Data(n, 2);
// //Modbus_Process();
// }
int16_t pos1;
int16_t pos2;
int16_t pos3;
float length1;
float length2;
float length3;
Encoder_Reset1(); // 复位计数器
Encoder_Reset2(); // 复位计数器
Encoder_Reset3(); // 复位计数器
while(1)
{
while(flag == 1)//停止旋转,读取霍尔寄存器地址
{
pos1 = Encoder_Get1(); // 获取脉冲数(带方向)
pos2 = Encoder_Get2(); // 获取脉冲数(带方向)
pos3 = Encoder_Get3(); // 获取脉冲数(带方向)
length1 = pos1 / 144.0f; // 假设 20 脉冲 = 1 mm
length2 = pos2 / 144.0f;
length3 = pos3 / 144.0f;
Holding_Reg[0] = 0x0051;
Holding_Reg[1] = 0x0032;
Holding_Reg[2] = 0x0020;
if(flag == 2)
{
break;
}
}
while(flag == 2)//慢速旋转
{
for(i = 0; i<180; i++)
{
Set_Servo_Angle(1, i);
Set_Servo_Angle(2, i);
Set_Servo_Angle(3, i);
Delay_ms(50);
if(flag != 2) break;
}
if(flag != 2)
{
Set_Servo_Angle(1, 90);
Set_Servo_Angle(2, 90);
Set_Servo_Angle(3, 90);
Delay_ms(1000);
break;
}
for(i = 180; i>0; i--)
{
Set_Servo_Angle(1, i);
Set_Servo_Angle(2, i);
Set_Servo_Angle(3, i);
Delay_ms(50);
if(flag != 2) break;
}
if(flag != 2)
{
Set_Servo_Angle(1, 90);
Set_Servo_Angle(2, 90);
Set_Servo_Angle(3, 90);
Delay_ms(1000);
break;
}
}
while(flag == 3)//快速旋转
{
Set_Servo_Angle(1, 0);
Set_Servo_Angle(2, 0);
Set_Servo_Angle(3, 0);
Delay_ms(2000);
if(flag != 3)
{
Set_Servo_Angle(1, 90);
Set_Servo_Angle(2, 90);
Set_Servo_Angle(3, 90);
Delay_ms(1000);
break;
}
Set_Servo_Angle(1, 180);
Set_Servo_Angle(2, 180);
Set_Servo_Angle(3, 180);
Delay_ms(2000);
if(flag != 3)
{
Set_Servo_Angle(1, 90);
Set_Servo_Angle(2, 90);
Set_Servo_Angle(3, 90);
Delay_ms(1000);
break;
}
}
}
}
我在写第一个版本的时候遇到了两个问题,一个是不能返回数据,另一个是接受数据后卡死;
第一个是因为USART_ClearITPendingBit(USART2, USART_IT_RXNE);这个删除标志位的代码,上面接收完数据后会自动删除串口中断标志位,所以我加入这段代码意味着我将下一次中断删除了,会加入0xff的干扰,导致不能返回数据
第二个接收数据后卡死是因为在处理功能码为06写入时,校验位赋值错误,导致终端里面计算错误失效,导致终端卡死。
三、PLC发送代码
首先,进行配置通讯

其次,配置主站

最后,就可以运行啦,运行的时候你会发现,非常的流畅,不会中断,好用的很------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------(●ˇ∀ˇ●)------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------O(∩_∩)O------------------------------------------------------------------------------