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 ≈ |
|---|---|
| 1200 | 32.1 ms |
| 2400 | 16.0 ms |
| 4800 | 8.02 ms |
| 9600 | 4.01 ms |
| 19200 | 1.75ms |
| 38400 | 1.75ms |
| 115200 | 1.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~065536 | 0000H~FFFFH | 01、05、15 | 可读可写 |
| 离散输入状态 | 100001~165536 | 0000H~FFFFH | 02 | 只读 |
| 保持寄存器 | 400001~465536 | 0000H~FFFFH | 03、06、16、23 | 可读可写 |
| 输入寄存器 | 300001~365536 | 0000H~FFFFH | 04 | 只读 |
| 功能码 | 功能 | 寄存器编号 | 寄存器地址 | 位 / 字操作 | 操作数量 |
|---|---|---|---|---|---|
| 01 | 读线圈状态 | 000001~065536 | 0000H~FFFFH | 位操作 | 读 1~2000 |
| 02 | 读离散输入状态 | 100001~165536 | 0000H~FFFFH | 位操作 | 读 1~2000 |
| 03 | 读保持寄存器 | 400001~465536 | 0000H~FFFFH | 字操作 | 读 1~125 |
| 04 | 读输入寄存器 | 300001~365536 | 0000H~FFFFH | 字操作 | 读 1~125 |
| 05 | 写单个线圈 | 000001~065536 | 0000H~FFFFH | 位操作 | 写 1 |
| 06 | 写单个保持寄存器 | 400001~465536 | 0000H~FFFFH | 字操作 | 写 1 |
| 15 | 写单个或多个线圈 | 000001~065536 | 0000H~FFFFH | 位操作 | 写 1~1968 |
| 16 | 写单个或多个保持寄存器 | 400001~465536 | 0000H~FFFFH | 字操作 | 写 1~123 |
| 23 | 读写单个或多个保持寄存器 | 400001~465536 | 0000H~FFFFH | 字操作 | 读 1~125、写 1~121 |
配置STM32CubeMX
首先使用CubeMX生成工程文件,这里使用的芯片是STM32F103ZET6。
配置时钟,设置为外部时钟源,主频设置为72MHz。

配置串口,这里使用的是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屏幕,不需要可以不配置。
以上,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 | extern TIM_HandleTypeDef htim2;extern UART_HandleTypeDef huart3;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 | 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); |
Modbus头文件。
验证
首先在mian.c中,添加Modbus的include
1234 | /* USER CODE BEGIN Includes *//* 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去读取。
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设定的寄存器值。
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_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_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_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验证。