STM32 HAL库实现Modbus RTU主站

Modbus协议

RS-485和RS-232是硬件层协议,是一种电平标准。
Modbus是软件层协议。
RS-232电平标准:+3V~+15V对应0,-3V~-15V对应1。
RS-485电平标准:A线和B线,两线的压差为-2V~-6V表示0,两线压差为+2V~+6V表示1。
RS-485采用的是差分信号,适合长距离传输,抗干扰比RS-232强很多。

Modbus概述

Modbus是一种总线协议,主从通讯(问答式,必须是主机发问,从机回答,从机没有权限发问)。是一种局域网协议,最早是施耐德为了PLC通讯而发布。

Modbus协议分类

Modbus-RTU:默认的Modbus协议,对应串口的HEX16进制接收模式。
Modbus-ASCII:阅读性比Modbus-RTU好,对应串口的ASCII协议
Modbus-TCP:Modbus在TCP上的协议
Modbus-PLUS:高速现场总线协议,使用令牌方式传输

Modbus-RTU数据格式

Modbus-RTU协议通讯中每个字符的传输格式规定为:1个起始位,8个数据位,1个校验位(偶/奇/无,若无校验则停止位为 2),1个或2个停止位,1个字符是11个位。
这里提到1个字符对应11位,是因为判断Modbus一帧数据传输结束是根据3.5个字符间隔来区分,而每个字符传输的时间是根据波特率的快慢。
3.5x11=38.5位,如果按照9600波特率来计算,即1000ms发送9600位,那么发送38.5位需要4.01ms时间,需要一个定时器来计时。
(需要开启单片机的串口、定时器)
如果波特率小于9600bps,两个数据帧之间至少要有3.5个字符时间间隔。
波特率大于等于19200bps时,两个数据帧之间间隔,以波特率为19200bps时的3.5个字符长度为准,即1750us

波特率T3.5 ≈
120032.1 ms
240016.0 ms
48008.02 ms
96004.01 ms
192001.75ms
384001.75ms
1152001.75ms

报文内容:
地址码:从站地址,范围1-254
功能码:区分不同的功能
数据区:不同功能码,数据不同
校验码:CRC校验,校验范围是地址码开始到数据码结束

起始地址功能代码数据CRC校验结束
≥3.5字符8位8位Nx8位16位≥3.5字符

寄存器1个地址对应1个寄存器。1个寄存器用2个字节(16位)保存数据。

Modbus功能码

寄存器种类说明与 PLC 比较举例说明
线圈状态
(Coil Status)
数字量输出、继电器输出,可读可写DO 数字量输出或者内部读写位变量电磁阀输出、继电器
离散输入状态
(Input Status)
数字量输入,只读DI 数字量输入或者内部只读位变量按钮输入、拨码开关、接近开关
保持寄存器
(Holding Register)
输出参数、保持参数,可读可写AO 模拟量输出或者内部读写寄存器模拟量输出设定、变量阀输出大小
输入寄存器
(Input Register)
输入参数,只读AI 模拟量输入或者内部只读寄存器模拟量输入,现场工程量信号采集
寄存器种类寄存器编号寄存器地址支持功能码读写状态
线圈状态000001~0655360000H~FFFFH01、05、15可读可写
离散输入状态100001~1655360000H~FFFFH02只读
保持寄存器400001~4655360000H~FFFFH03、06、16、23可读可写
输入寄存器300001~3655360000H~FFFFH04只读
功能码功能寄存器编号寄存器地址位 / 字操作操作数量
01读线圈状态000001~0655360000H~FFFFH位操作读 1~2000
02读离散输入状态100001~1655360000H~FFFFH位操作读 1~2000
03读保持寄存器400001~4655360000H~FFFFH字操作读 1~125
04读输入寄存器300001~3655360000H~FFFFH字操作读 1~125
05写单个线圈000001~0655360000H~FFFFH位操作写 1
06写单个保持寄存器400001~4655360000H~FFFFH字操作写 1
15写单个或多个线圈000001~0655360000H~FFFFH位操作写 1~1968
16写单个或多个保持寄存器400001~4655360000H~FFFFH字操作写 1~123
23读写单个或多个保持寄存器400001~4655360000H~FFFFH字操作读 1~125、写 1~121

配置STM32CubeMX

首先使用CubeMX生成工程文件,这里使用的芯片是STM32F103ZET6。
配置时钟,设置为外部时钟源,主频设置为72MHz。
配置STM32CubeMX
配置STM32CubeMX
配置串口,这里使用的是USART3串口,设置为Asynchronous异步模式,波特率设置为9600,字长8位,无校验位,停止位1位
配置串口
开启一个时钟。TIM2挂载在APB1总线上,前面设置的主频是72MHz,预分频器PSC的值设置为72-1,因此经过分频后得到的时钟频率为1MHz。
计数器的奇数方向为上计数。
自动重装寄存器ARR,设置为1000-1,此时定时器的定时周期为1000。
因此溢出的频率为1MHZ÷1000=1kHz,这样每1ms产生一次update事件。
配置时钟
配置中断,开启TIM2和串口的中断,这里串口的优先级要比计时器的优先级高。
配置中断
配置一个GPIO输出,这里配置的是PE3,用来控制MAX485的RE和DE引脚,发送和接收时改变PE3的状态,发送数据时,让RE和DE悬空,接收数据时,RE和DE都接地。
另外这里的PB6和PB7用来驱动OLED屏幕,不需要可以不配置。
配置GPIO
以上,CubeMX的配置完毕,接下来使用VS Code编写Modbus代码。在Project Manger里配置Toolchain/IDE为CMake。

Modbus RTU主站代码

Modbus源码下载
在工程根目录下,创建Modbus文件夹,再创建Modbus.c和Modbus.h文件。
目录
然后打开CmakeList.txt,添加头文件路径和源文件。
在 target_include_directories 部分,添加头文件所在目录
在 target_sources 部分,添加源文件。
下面的代码还包含OLED的头文件和源文件,不需要使用可以去掉。

CmakeList.txt

123456789101112
# Add sources to executabletarget_sources(${CMAKE_PROJECT_NAME} PRIVATE    # Add user sources here    ${CMAKE_SOURCE_DIR}/Modbus/Modbus.c    ${CMAKE_SOURCE_DIR}/OLED/OLED.c)# Add include pathstarget_include_directories(${CMAKE_PROJECT_NAME} PRIVATE    # Add user defined include paths    ${CMAKE_SOURCE_DIR}/Modbus    ${CMAKE_SOURCE_DIR}/OLED)

Modbus.c

123456789
#include "Modbus.h"extern TIM_HandleTypeDef  htim2;extern UART_HandleTypeDef huart3;#define RX_BUFFER_SIZE 256 //接收缓冲区大小#define Frame_Timeout_ms 4 //帧间隔超时时间,单位为毫秒uint8_t rx_buffer[RX_BUFFER_SIZE]; //接收缓冲区uint8_t rx_index = 0; //接收缓冲区索引uint32_t last_rx_time = 0; //上次接收数据的时间戳static uint8_t *current_buffer = NULL; //当前数据缓冲区指针,用于存储接收到的数据

这里需要注意,Frame_Timeout_ms的时间由波特率决定,由于设定的波特率为9600,因此这里设置为4。

123456
//Modbus初始化函数void Modbus_Init(void){    HAL_TIM_Base_Start_IT(&htim2); //启动定时器2中断    HAL_UART_Receive_IT(&huart3, &rx_buffer[rx_index], 1); //启动串口3接收中断}

初始化Modbus函数,先启动定时器中断,再启动串口3接收中断

1234567891011121314151617181920
//Modbus接收数据回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){    if (huart->Instance == USART3) //检查是否为串口3    {        //检查缓冲区是否已满        if (rx_index < RX_BUFFER_SIZE - 1)        {            last_rx_time = HAL_GetTick(); //更新接收时间戳            rx_index++; //接收到的数据存储到缓冲区,索引加1            HAL_UART_Receive_IT(&huart3, &rx_buffer[rx_index], 1); //继续接收下一个字节        }        else        {            //缓冲区已满,丢弃数据并重置索引            rx_index = 0;            HAL_UART_Receive_IT(&huart3, &rx_buffer[rx_index], 1); //重新开始接收        }    }}

处理串口单个字节接收完成的中断,将收到的字节存入rx_buffer缓冲区,并立即重新启动下一次单字节接收,从而实现连续接收。
if (huart->Instance == USART3)是为了判断是否为串口3的数据。

12345678910111213141516171819202122
//Modbus CRC16校验计算函数uint16_t Modbus_CRC16(uint8_t *data, uint16_t length){    uint16_t crc = 0xFFFF; //初始化CRC寄存器    for (uint16_t i = 0; i < length; i++)    {        crc ^= data[i]; //将数据与CRC寄存器进行异或运算        for (uint8_t j = 0; j < 8; j++)        {            if (crc & 0x0001) //检查最低位是否为1            {                crc >>= 1; //右移一位                crc ^= 0xA001; //与多项式进行异或运算            }            else            {                crc >>= 1; //右移一位            }        }    }    return crc; //返回计算得到的CRC值}

CRC16校验。

1234567891011121314
//Modbus发送数据函数void Modbus_Send(uint8_t *data, uint16_t length){    // 发送前:使能发送,禁止接收    HAL_GPIO_WritePin(GPIOE, GPIO_PIN_3, GPIO_PIN_SET);    //通过串口3发送数据    HAL_UART_Transmit(&huart3, data, length, HAL_MAX_DELAY);    //等待发送完成    while (__HAL_UART_GET_FLAG(&huart3, UART_FLAG_TC) == RESET);    //等待发送完成标志位被置位    HAL_Delay(10);    // 发送后:禁止发送,使能接收    HAL_GPIO_WritePin(GPIOE, GPIO_PIN_3, GPIO_PIN_RESET);}

Modbus发送,发送前先拉高PE13,进入发送状态,调用HAL_UART_Transmit发送数据,等待发送完成后,再拉低PE13,禁止发送,使能接收。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
//Modbus处理接收到的数据函数void Modbus_Handle_Rx(uint8_t *data, uint8_t length){    //crc校验    uint16_t received_crc = (data[length - 1] << 8) | data[length - 2]; //接收到的CRC值    uint16_t calculated_crc = Modbus_CRC16(data, length - 2);//计算得到的CRC值,排除最后两个字节的CRC    if (received_crc != calculated_crc)    {        return; //CRC校验失败,丢弃数据    }     //解析从站返回的数据,根据功能码进行处理    uint8_t slave_addr = data[0]; //从站地址    uint8_t function_code = data[1]; //功能码     if (function_code == 0x01)//功能码01处理    {        //解析字节数和线圈数量        uint8_t byte_count = data[2]; //字节计数        memcpy(current_buffer, &data[3], byte_count); //将接收到的数据复制到当前缓冲区    }    else if (function_code == 0x03)//功能码03处理    {        //解析字节数和寄存器数量        uint8_t byte_count = data[2]; //字节计数        memcpy(current_buffer, &data[3], byte_count); //将接收到的数据复制到当前缓冲区    }    else if(function_code == 0x05)    {        //检查响应帧长度(固定6字节数据+2字节CRC)        if (length != 8)        {            return;        }        //解析线圈地址和线圈值        uint16_t coil_addr = (data[2] << 8) | data[3]; //线圈地址        uint16_t coil_value = (data[4] << 8) | data[5]; //线圈值    }    else if(function_code == 0x06)    {        //检查响应帧长度(固定6字节数据+2字节CRC)        if (length != 8)        {            return;        }        //解析寄存器地址和寄存器值        uint16_t reg_addr = (data[2] << 8) | data[3]; //寄存器地址        uint16_t reg_value = (data[4] << 8) | data[5]; //寄存器值    }    else if(function_code == 0x0F)    {        //检查响应帧长度(固定6字节数据+2字节CRC)        if (length != 8)        {            return;        }        //解析起始线圈地址和线圈数量        uint16_t start_addr = (data[2] << 8) | data[3]; //起始线圈地址        uint16_t num_coils = (data[4] << 8) | data[5]; //线圈数量    }    else if(function_code == 0x10)    {        //检查响应帧长度(固定6字节数据+2字节CRC)        if (length != 8)        {            return;        }        //解析起始寄存器地址和寄存器数量        uint16_t start_addr = (data[2] << 8) | data[3]; //起始寄存器地址        uint16_t num_regs = (data[4] << 8) | data[5]; //寄存器数量    }    else if(function_code >= 0x80)    {        //异常响应处理        uint8_t error_code = data[2]; //异常码    }    else    {        ;    }}

首先取出接收到报文的最后2个字节取出接收到的CRC,然后去掉最后2个字节,计算CRC,进行比较,确认CRC是否相同。
根据不同的功能码进行不同的处理。

123456789101112131415161718
//Modbus定时器中断回调函数void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){    if(htim->Instance == TIM2)    {        //检查是否有数据接收,并且距离上次接收时间超过帧间隔超时时间        if (HAL_GetTick() - last_rx_time >= Frame_Timeout_ms)        {            //处理接收到的数据            if (rx_index > 0)            {                Modbus_Handle_Rx(rx_buffer, rx_index); //调用处理函数,传入接收缓冲区和数据长度                //处理完数据后,重置接收缓冲区索引                rx_index = 0;            }        }    }}

if(htim->Instance == TIM2)首先确认是否为TIM2的回调。
再判断上次接收到的时间和本次接收到的时间,是否大于超时时间,即3.5字符的间隔。
如果缓冲区中有数据(rx_index 不为 0),表示确实收到了一个完整的帧(至少 1 字节)。
调用 Modbus_Handle_Rx 进行 CRC 校验和功能码解析,传入缓冲区和实际字节数。
处理完成后,将索引置 0,清空缓冲区,等待下一帧。

123456789101112131415161718192021
//01功能码,Modbus读取线圈状态void Modbus_Read_01(uint8_t slave_addr, uint16_t start_addr, uint16_t num_coils, uint8_t *user_buffer){    uint8_t TX_Frame[8];    uint16_t crc;     current_buffer = user_buffer; //设置当前数据缓冲区,将user_buffer指针传递给current_buffer,以便在接收数据时将数据存储到正确的缓冲区中    //1.构建Modbus RTU请求帧    TX_Frame[0] = slave_addr; //从机地址    TX_Frame[1] = 0x01; //功能码    TX_Frame[2] = (start_addr >> 8) & 0xFF; //起始地址高字节    TX_Frame[3] = start_addr & 0xFF; //起始地址低字节    TX_Frame[4] = (num_coils >> 8) & 0xFF; //线圈数量高字节    TX_Frame[5] = num_coils & 0xFF; //线圈数量低字节    //2.计算CRC校验码    crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码    TX_Frame[6] = crc & 0xFF; //CRC低字节    TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节    //3.发送请求帧    Modbus_Send(TX_Frame, 8); //发送数据帧}

01功能码,用于读取线圈。
首先,定义一个数组TX_Frame[8],并把current_buffer设定为传入的user_buffer。
构建长度为8的请求帧,根据01功能码的定义,这里的数组长度一定是8。

123456789101112131415161718192021
//03功能码,Modbus读取保持寄存器void Modbus_Read_03(uint8_t slave_addr, uint16_t start_addr, uint16_t num_regs,uint8_t *user_buffer){    uint8_t TX_Frame[8];    uint16_t crc;     current_buffer = user_buffer; //设置当前数据缓冲区,将user_buffer指针传递给current_buffer,以便在接收数据时将数据存储到正确的缓冲区中    //1.构建Modbus RTU请求帧    TX_Frame[0] = slave_addr; //从机地址    TX_Frame[1] = 0x03; //功能码    TX_Frame[2] = (start_addr >> 8) & 0xFF; //起始地址高字节    TX_Frame[3] = start_addr & 0xFF; //起始地址低字节    TX_Frame[4] = (num_regs >> 8) & 0xFF; //寄存器数量高字节    TX_Frame[5] = num_regs & 0xFF; //寄存器数量低字节    //2.计算CRC校验码    crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码    TX_Frame[6] = crc & 0xFF; //CRC低字节    TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节    //3.发送请求帧    Modbus_Send(TX_Frame, 8); //发送数据帧}

03功能码,读取保持寄存器。
这里和01功能码差不多,不多做解释。

1234567891011121314151617181920
//05功能码,Modbus写单个线圈void Modbus_Write_05(uint8_t slave_addr, uint16_t coil_addr, uint16_t coil_value){    uint8_t TX_Frame[8];    uint16_t crc;     //1.构建Modbus RTU请求帧    TX_Frame[0] = slave_addr; //从机地址    TX_Frame[1] = 0x05; //功能码    TX_Frame[2] = (coil_addr >> 8) & 0xFF; //线圈地址高字节    TX_Frame[3] = coil_addr & 0xFF; //线圈地址低字节    TX_Frame[4] = (coil_value == 0) ? 0x00 : 0xFF; //线圈值,0x00表示关闭,0xFF表示开启    TX_Frame[5] = 0x00; //线圈值低字节,固定为0x00    //2.计算CRC校验码    crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码    TX_Frame[6] = crc & 0xFF; //CRC低字节    TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节    //3.发送请求帧    Modbus_Send(TX_Frame, 8); //发送数据帧}

05功能码,Modbus写单个线圈。

1234567891011121314151617181920
//06功能码,Modbus写单个保持寄存器void Modbus_Write_06(uint8_t slave_addr, uint16_t reg_addr, uint16_t reg_value){    uint8_t TX_Frame[8];    uint16_t crc;     //1.构建Modbus RTU请求帧    TX_Frame[0] = slave_addr; //从机地址    TX_Frame[1] = 0x06; //功能码    TX_Frame[2] = (reg_addr >> 8) & 0xFF; //寄存器地址高字节    TX_Frame[3] = reg_addr & 0xFF; //寄存器地址低字节    TX_Frame[4] = (reg_value >> 8) & 0xFF; //寄存器值高字节    TX_Frame[5] = reg_value & 0xFF; //寄存器值低字节    //2.计算CRC校验码    crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码    TX_Frame[6] = crc & 0xFF; //CRC低字节    TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节    //3.发送请求帧    Modbus_Send(TX_Frame, 8); //发送数据帧}

06功能码,Modbus写单个保持寄存器
根据传入的16位数值,分成2个8位的字节,放到请求帧中。

1234567891011121314151617181920212223242526272829
//15功能码,Modbus写多个线圈void Modbus_Write_15(uint8_t slave_addr, uint16_t start_addr, uint16_t num_coils_regs, uint8_t *coil_values){    uint8_t TX_Frame[9 + num_coils_regs]; //根据需要调整缓冲区大小    uint16_t crc;     //1.构建Modbus RTU请求帧    TX_Frame[0] = slave_addr; //从机地址    TX_Frame[1] = 0x0F; //功能码    TX_Frame[2] = (start_addr >> 8) & 0xFF; //起始地址高字节    TX_Frame[3] = start_addr & 0xFF; //起始地址低字节    TX_Frame[4] = (num_coils_regs >> 8) & 0xFF; //线圈数量高字节    TX_Frame[5] = num_coils_regs & 0xFF; //线圈数量低字节    TX_Frame[6] = (num_coils_regs + 7) / 8; //字节计数     //将线圈值填充到请求帧中    for (uint16_t i = 0; i < (num_coils_regs + 7) / 8; i++)    {        TX_Frame[7 + i] = coil_values[i]; //将线圈值复制到请求帧中    }     //2.计算CRC校验码    crc = Modbus_CRC16(TX_Frame, 7 + (num_coils_regs + 7) / 8); //计算CRC校验码    TX_Frame[7 + (num_coils_regs + 7) / 8] = crc & 0xFF; //CRC低字节    TX_Frame[8 + (num_coils_regs + 7) / 8] = (crc >> 8) & 0xFF; //CRC高字节     //3.发送请求帧    Modbus_Send(TX_Frame, 9 + (num_coils_regs + 7) / 8); //发送数据帧      }

15功能码,Modbus写多个线圈。
这里的请求帧长度由写入的线圈数量决定。

123456789101112131415161718192021222324252627282930
//16功能码,Modbus写多个保持寄存器void Modbus_Write_16(uint8_t slave_addr, uint16_t start_addr, uint16_t num_regs, uint16_t *reg_values){    uint8_t TX_Frame[9+2*num_regs]; //根据需要调整缓冲区大小,长度为9+2*寄存器数量    uint16_t crc;     //1.构建Modbus RTU请求帧    TX_Frame[0] = slave_addr; //从机地址    TX_Frame[1] = 0x10; //功能码    TX_Frame[2] = (start_addr >> 8) & 0xFF; //起始地址高字节    TX_Frame[3] = start_addr & 0xFF; //起始地址低字节    TX_Frame[4] = (num_regs >> 8) & 0xFF; //寄存器数量高字节    TX_Frame[5] = num_regs & 0xFF; //寄存器数量低字节    TX_Frame[6] = num_regs * 2; //字节计数,因为每个寄存器占用2个字节     //将寄存器值填充到请求帧中    for (uint16_t i = 0; i < num_regs; i++)    {        TX_Frame[7 + i * 2] = (reg_values[i] >> 8) & 0xFF; //寄存器值高字节        TX_Frame[8 + i * 2] = reg_values[i] & 0xFF; //寄存器值低字节    }     //2.计算CRC校验码    crc = Modbus_CRC16(TX_Frame, 7 + 2 * num_regs); //计算CRC校验码    TX_Frame[7 + 2 * num_regs] = crc & 0xFF; //CRC低字节    TX_Frame[8 + 2 * num_regs] = (crc >> 8) & 0xFF; //CRC高字节     //3.发送请求帧    Modbus_Send(TX_Frame, 9 + 2 * num_regs); //发送数据帧      }

16功能码,Modbus写多个保持寄存器。
这里的帧长度,同样由寄存器数量决定。
TX_Frame[9+2*num_regs]每个16位寄存器的值,需要拆分为2个字节传输
之后由循环写入到帧中。

1234
int16_t Modbus_MergeHighLow(uint8_t high, uint8_t low){    return ((high << 8) | low); //将高字节和低字节合并为一个16位有符号整数}

这个函数的作用主要是把2个接收到的字节,合并为一个16位的数字,主要是03使用。

Modbus.h

1234567891011121314151617
#ifndef __MODBUS_H#define __MODBUS_H #include "main.h"#include "string.h" void Modbus_Init(void);void Modbus_Read_01(uint8_t slave_addr, uint16_t start_addr, uint16_t num_coils, uint8_t *user_buffer);void Modbus_Read_03(uint8_t slave_addr, uint16_t start_addr, uint16_t num_regs,uint8_t *user_buffer);void Modbus_Write_05(uint8_t slave_addr, uint16_t coil_addr, uint16_t coil_value);void Modbus_Write_06(uint8_t slave_addr, uint16_t reg_addr, uint16_t reg_value);void Modbus_Write_15(uint8_t slave_addr, uint16_t start_addr, uint16_t num_coils_regs, uint8_t *coil_values);void Modbus_Write_16(uint8_t slave_addr, uint16_t start_addr, uint16_t num_regs, uint16_t *reg_values); int16_t Modbus_MergeHighLow(uint8_t high, uint8_t low);  #endif 

Modbus头文件。

验证

首先在mian.c中,添加Modbus的include

1234
/* USER CODE BEGIN Includes */#include "Modbus.h"#include "OLED.h"/* USER CODE END Includes */

在while(1)循环之前,初始化Modbus和OLED

12
  Modbus_Init();//Modbus初始化  OLED_Init();//OLED初始化

Modbus_Read_01

这是读取线圈状态的功能。

12345
/* USER CODE BEGIN 2 */Modbus_Init();//Modbus初始化OLED_Init();//OLED初始化uint8_t modbus01_buffer[5]; //定义一个数组用于存储Modbus读取01功能码返回的数据/* USER CODE END 2 */
12345678910
while (1){  Modbus_Read_01(0x01, 0x00, 16, modbus01_buffer);   HAL_Delay(1000); //延时1秒  OLED_ShowString(1,1,"Modbus Test");   OLED_ShowBinNum(2,1,modbus01_buffer[0],8);  OLED_ShowBinNum(3,1,modbus01_buffer[1],8);  /* USER CODE END WHILE */  /* USER CODE BEGIN 3 */}

Modbus的01功能码读线圈状态,读01从站,从0开始读16个,保存在modbus01_buffer数组中。
每1s读取一次,并使用OLED显示出来。

使用Modbus Slave模拟一个从站,使用STM32去读取。
Modbus_Read_01_MBSlave
OLED显示的内容。
Modbus_Read_01_OLED
Modbus Slave设定的线圈值和STM32读取到的值相同。

Modbus_Read_03

这是Modbus读取寄存器的功能。

12345
  /* USER CODE BEGIN 2 */  Modbus_Init();//Modbus初始化  OLED_Init();//OLED初始化  uint8_t modbus03_buffer[5]; //定义一个数组用于存储Modbus读取03功能码返回的数据  /* USER CODE END 2 */
12345678910
while (1){  Modbus_Read_03(0x01, 0x00, 0x0002, modbus03_buffer);   HAL_Delay(1000); //延时1秒  OLED_ShowString(1,1,"Modbus Test");   OLED_ShowSignedNum(2,1,Modbus_MergeHighLow(modbus03_buffer[0], modbus03_buffer[1]),5);  OLED_ShowSignedNum(3,1,Modbus_MergeHighLow(modbus03_buffer[2], modbus03_buffer[3]),5);  /* USER CODE END WHILE */  /* USER CODE BEGIN 3 */}

Modbus的03读寄存器功能,读01从站,从0开始读,读2个寄存器,保存在modbus03_buffer中。
由于每个寄存器的值分为高位和低位保存,需要使用Modbus_MergeHighLow来合并为一个16位的值。

Modbus Slave设定的寄存器值。
Modbus_Read_03_MBSlave

OLED显示的内容
!Modbus_Read_03_OLED

Modbus_Write_05

写单个线圈。

1234567
while (1){  Modbus_Write_05(0x01, 0x05, 1);  HAL_Delay(1000); //延时1秒  /* USER CODE END WHILE */  /* USER CODE BEGIN 3 */}

使用05功能码,把站号为1的线圈5,写1。

查看Modbus Slave,地址为5的线圈,被置ON。
Modbus_Write_05_MBSlave

Modbus_Write_06

写单个寄存器。

123456789
while (1){  Modbus_Write_06(0x01, 0x00, 12345);   HAL_Delay(1000); //延时1秒  Modbus_Write_06(0x01, 0x01, -23456);   HAL_Delay(1000); //延时1秒  /* USER CODE END WHILE */  /* USER CODE BEGIN 3 */}

地址是0的寄存器写12345,地址是1的寄存器写-23456。

查看Modbus Slave,成功写入。
Modbus_Write_06_MBSlave

Modbus_Write_15

写多个线圈。

1234567
  /* USER CODE BEGIN 2 */  Modbus_Init();//Modbus初始化  OLED_Init();//OLED初始化  uint8_t Write_Value[5] = {0};   Write_Value[0] = 0B00001111;  Write_Value[1] = 0B10010010;  /* USER CODE END 2 */
1234567
while (1){  Modbus_Write_15(0x01,0x00,16,Write_Value);  HAL_Delay(1000); //延时1秒  /* USER CODE END WHILE */  /* USER CODE BEGIN 3 */}

把Write_Value的值写入到线圈中。

查看Modbus Slave的线圈值。
Modbus_Write_15_MBSlave

Modbus_Write_16

多个寄存器写。

1234567
  /* USER CODE BEGIN 2 */  Modbus_Init();//Modbus初始化  OLED_Init();//OLED初始化  uint16_t Write_Value[5] = {0};   Write_Value[0] = 12345;  Write_Value[1] = -23456;  /* USER CODE END 2 */
1234567
while (1){  Modbus_Write_16(0x01,0x00,0x02,Write_Value);  HAL_Delay(1000); //延时1秒  /* USER CODE END WHILE */  /* USER CODE BEGIN 3 */}

把12345和-23456分别写入到地址为0的寄存器和地址为1的寄存器。

使用Modbus Slave验证。
Modbus_Write_16_MBSlave

🔍 ×