目录
使用 CubeMX 实现 Modbus 从机(Slave)的开发主要涉及到以下几个步骤:
1. 硬件准备
你需要一块支持 Modbus 通信的 STM32 开发板,建议使用带有 USART 外设的 STM32 MCU,因为 Modbus 通常是通过 RS485 串口通信实现的。同时,RS485 通信还需要使用相应的驱动芯片(如 MAX485)来转换电平。
2. CubeMX 配置步骤
-
打开 CubeMX 并创建一个新的 STM32 工程。
-
选择芯片或开发板:选择你所使用的 STM32 芯片。
-
配置串口(USART/RS485):
- 打开串口(比如 USART1/USART2),并将模式设置为异步(Asynchronous)。
- 设置波特率为 Modbus 标准支持的波特率(如 9600、115200等)。
- 启用中断模式以处理接收和发送的数据。
-
配置 GPIO 引脚:如果使用 RS485 通信,需要配置一个控制 DE(Driver Enable)引脚来控制 RS485 驱动器的发送和接收模式。通常,RS485 驱动器有 DE 和 RE 引脚(RE 可以与 DE 连接在一起)。配置一个 GPIO 引脚输出高电平时为发送,低电平时为接收。
-
时钟配置:确保串口所需的时钟源已正确配置。
-
生成代码:完成配置后,点击“Project -> Generate Code”生成代码。
3. 集成 Modbus 库
实现 03 06 和 10 三个功能码 读写保持寄存器
4. 编写应用代码
modbus.c
#include "modbus.h"
#include <stdint.h>
#include "stm32f1xx_hal.h" // 需要包含 STM32 HAL 库头文件
UART_HandleTypeDef *g_huart;
// 保持寄存器数组
uint16_t holding_registers[REGISTER_COUNT] __attribute__((aligned(2))); // 确保内存对齐
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint16_t rx_index = 0; // 当前接收到的字节索引
// CRC16 查找表
uint16_t crc16_table[256];
// 初始化 CRC16 查找表
void Modbus_InitCRC16Table() {
uint16_t crc;
for (uint16_t i = 0; i < 256; i++) {
crc = i;
for (uint8_t j = 8; j > 0; j--) {
if (crc & 0x01) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
crc16_table[i] = crc;
}
}
// 计算 CRC16 校验值
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
uint8_t index = (crc ^ data[i]) & 0xFF;
crc = (crc >> 8) ^ crc16_table[index];
}
return crc;
}
// 发送异常响应
void Modbus_SendException(uint8_t slave_addr, uint8_t function_code, uint8_t exception_code) {
uint8_t response[5]; // 最大需要 5 字节(3 字节 + 2 字节 CRC)
response[0] = slave_addr;
response[1] = function_code | 0x80; // 异常响应功能码
response[2] = exception_code; // 异常码
// 计算 CRC
uint16_t response_crc = Modbus_CRC16(response, 3);
response[3] = (response_crc >> 8) & 0xFF;
response[4] = response_crc & 0xFF;
// 确保不发送额外的换行符 \r\n
HAL_UART_Transmit(g_huart, response, 5, HAL_MAX_DELAY);
}
// 处理 Modbus RTU 帧
void Modbus_ProcessFrame(uint8_t *frame, uint16_t length) {
if (length < 8) return; // 帧长度至少为 8 字节
uint8_t slave_addr = frame[0];
uint8_t function_code = frame[1];
uint16_t start_addr = (frame[2] << 8) | frame[3];
uint16_t quantity = (frame[4] << 8) | frame[5];
uint16_t received_crc = (frame[length - 2] << 8) | frame[length - 1];
uint16_t calculated_crc = Modbus_CRC16(frame, length - 2);
if (received_crc != calculated_crc) {
return; // CRC 校验失败
}
if (slave_addr != slave_Addr) return; // 假设从机地址为 1
uint8_t response[RX_BUFFER_SIZE]; // 最大响应长度为 256
uint16_t response_length = 0;
uint16_t value = 0; // 确保初始化变量
uint8_t byte_count = 0; // 确保初始化变量
switch (function_code) {
case 0x03: // 读多个保持寄存器
if (start_addr + quantity > REGISTER_COUNT) {
Modbus_SendException(slave_addr, function_code, 0x02); // 地址越界
return;
}
response[0] = slave_addr;
response[1] = function_code;
response[2] = quantity * 2;
response_length = 3;
for (uint16_t i = 0; i < quantity; i++) {
if (response_length + 2 > sizeof(response)) {
Modbus_SendException(slave_addr, function_code, 0x04); // 响应长度超过限制
return;
}
response[response_length++] = (holding_registers[start_addr + i] >> 8) & 0xFF;
response[response_length++] = holding_registers[start_addr + i] & 0xFF;
}
break;
case 0x06: // 写单个保持寄存器
if (start_addr >= REGISTER_COUNT) {
Modbus_SendException(slave_addr, function_code, 0x02); // 地址越界
return;
}
value = (frame[4] << 8) | frame[5]; // 初始化 value
holding_registers[start_addr] = value;
// 构造响应
response[0] = slave_addr;
response[1] = function_code;
response[2] = (start_addr >> 8) & 0xFF; // 起始地址的高字节
response[3] = start_addr & 0xFF; // 起始地址的低字节
response[4] = (value >> 8) & 0xFF; // 值的高字节
response[5] = value & 0xFF; // 值的低字节
response_length = 6; // 响应长度为6个字节
break;
case 0x10: // 写多个保持寄存器
if (start_addr + quantity > REGISTER_COUNT) {
Modbus_SendException(slave_addr, function_code, 0x02); // 地址越界
return;
}
byte_count = frame[6]; // 初始化 byte_count
if (byte_count != quantity * 2) {
Modbus_SendException(slave_addr, function_code, 0x03); // 数据值不匹配
return;
}
for (uint16_t i = 0; i < quantity; i++) {
value = (frame[7 + i * 2] << 8) | frame[7 + i * 2 + 1];
holding_registers[start_addr + i] = value;
}
response[0] = slave_addr;
response[1] = function_code;
response[2] = (start_addr >> 8) & 0xFF;
response[3] = start_addr & 0xFF;
response[4] = (quantity >> 8) & 0xFF;
response[5] = quantity & 0xFF;
response_length = 6;
break;
default:
Modbus_SendException(slave_addr, function_code, 0x01); // 非法功能码
return;
}
// 检查响应是否越界
if (response_length >= sizeof(response)) {
Modbus_SendException(slave_addr, function_code, 0x04); // 响应长度超过限制
return;
}
uint16_t response_crc = Modbus_CRC16(response, response_length);
response[response_length++] = (response_crc >> 8) & 0xFF;
response[response_length++] = response_crc & 0xFF;
HAL_UART_Transmit(g_huart, response, response_length, HAL_MAX_DELAY);
}
// UART 接收中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
// 处理接收到的帧
Modbus_ProcessFrame(rx_buffer, Size-2);//换行符占两个字节
// 重新启动 UART 接收
HAL_UARTEx_ReceiveToIdle_IT(huart, rx_buffer, RX_BUFFER_SIZE);
}
}
// 初始化 Modbus RTU 从机
void Modbus_Init(UART_HandleTypeDef *huart) {
g_huart = huart;
// 初始化保持寄存器
for (uint16_t i = 0; i < REGISTER_COUNT; i++) {
holding_registers[i] = 0;
}
// 初始化 CRC16 查找表
Modbus_InitCRC16Table();
HAL_UARTEx_ReceiveToIdle_IT(huart, rx_buffer, RX_BUFFER_SIZE); // 空闲中断接收
}
modbus.h
#ifndef __MODBUS_H
#define __MODBUS_H
#include "stm32f1xx_hal.h"
#define REGISTER_COUNT 10 //寄存器数量
#define RX_BUFFER_SIZE 256 //串口缓冲区
#define slave_Addr 0x01 //从机地址
// 初始化 Modbus RTU 从机
void Modbus_Init(UART_HandleTypeDef *huart);
// 处理接收到的 Modbus 帧
void Modbus_ProcessFrame(uint8_t *frame, uint16_t length);
// 发送异常响应
void Modbus_SendException(uint8_t slave_addr, uint8_t function_code, uint8_t exception_code);
// CRC16 计算
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length);
// 初始化 CRC16 查找表
void Modbus_InitCRC16Table(void);
#endif
main.c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 初始化 Modbus RTU 从机
Modbus_Init(&huart1);
while (1)
{
}
}