本文实现了01、03、05、06、10、16功能码,Modbus RTU只需要在主站发送询问时,从站才需要回复相应的信息。
有关CubeMX的配置,可以查看主站的文章,有详细的配置过程。
01、05、15这三种功能码都是对线圈进行操作的,即对布尔量的数据进行操作。(对应PLC中的M、HM等)
03、06、16这三种功能码都是对寄存器进行操作,即对一个字进行操作。(对应PLC中的D、HD等)
下面先介绍这几种功能码的报文格式,方便程序编写。
01功能码,读线圈指令
例如主站希望获取02号从站,线圈起始地址为1000(0x03E8)后10(0x000A)个连续线圈的状态,主站应该发送的报文为:
| 地址 | 功能码 | 线圈地址 (高字节) | 线圈地址 (低字节) | 线圈个数 (高字节) | 线圈个数 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 01 | 03 | E8 | 00 | 0A | 3C | 4E |
假设从站1000~1009号线圈的状态分别为:
| 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 |
|---|---|---|---|---|---|---|---|---|---|
| ON | ON | OFF | ON | ON | OFF | OFF | ON | OFF | ON |
那么,从站应该返回的报文为:
| 地址 | 功能码 | 字节数 | 数据内容 (字节一) | 数据内容 (字节二) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|
| 02 | 01 | 02 | 9B | 02 | 17 | 0D |
第一个字节9B就对应1007~1000,对应的二进制为1001 1011,第二个字节02对应1009~1008,对应的二进制为0000 0010。
03功能码,读寄存器指令
例如,主站希望获取02号从站,寄存器起始地址为2000(0x07D0)后5个连续寄存器的状态,主站应该发送的报文为:
| 地址 | 功能码 | 寄存器起始地址 (高字节) | 寄存器起始地址 (低字节) | 寄存器个数 (高字节) | 寄存器个数 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 03 | 07 | D0 | 00 | 05 | 85 | 77 |
假设从站的寄存器2000~2004的状态分别为:
| 2000 | 2001 | 2002 | 2003 | 2004 |
|---|---|---|---|---|
| +12345 | -12345 | 0 | -32768 | +32767 |
这里返回2000~2004这5个寄存器的数据给主站,每个寄存器分成2个字节发送给主站,分为高低位。从站返回的报文应该是:
| 地址 | 功能码 | 字节数 | 数据内容 (高字节) | 数据内容 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02 | 03 | 0A | 30 | 39 | CF | C7 | 00 | 00 | 80 | 00 | 7F | FF | AC | B7 |
05功能码,写单个线圈
例如,主站希望写02号从站,给地址为2000的线圈写1,主站应该发送的报文为:
| 地址 | 功能码 | 线圈地址 (高字节) | 线圈地址 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 05 | 07 | D0 | FF | 00 | 8C | 84 |
注意:使用05功能码写单个线圈时,如果写1(ON),第5、6个字节固定写FF00,如果写0,第5、6个字节固定写0000。
从站会原样返回主站发送的内容给主站确认:
| 地址 | 功能码 | 线圈地址 (高字节) | 线圈地址 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 05 | 07 | D0 | FF | 00 | 8C | 84 |
06功能码,写单个寄存器
例如,主站希望写02号从站,地址为500的寄存器写-2345,主站发送的报文为:
| 地址 | 功能码 | 寄存器地址 (高字节) | 寄存器地址 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 06 | 01 | F4 | F6 | D7 | CE | 09 |
从站会原样返回主站发送的内容给主站确认:
| 地址 | 功能码 | 寄存器地址 (高字节) | 寄存器地址 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 06 | 01 | F4 | F6 | D7 | CE | 09 |
15功能码,写多个线圈
例如,主站希望给02号从站的100~109号线圈,分别写入下面的值:
| 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
|---|---|---|---|---|---|---|---|---|---|
| ON | OFF | ON | OFF | ON | ON | OFF | OFF | ON | ON |
主站发送的报文为:
| 地址 | 功能码 | 线圈起始地址 (高字节) | 线圈起始地址 (低字节) | 线圈个数 (高字节) | 线圈个数 (低字节) | 字节数 | 数据内容 (低字节) | 数据内容 (高字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|---|---|---|
| 02 | 0F | 00 | 64 | 00 | 0A | 02 | 35 | 03 | AE | BD |
数据分为2个字发送,高位在前低位在后,因此数据的十六进制就是0335,转换为2进制为:1100110101,分别对应509~500号线圈。
从站接收到主站的报文后,会返回:
| 地址 | 功能码 | 线圈起始地址 (高字节) | 线圈起始地址 (低字节) | 线圈个数 (高字节) | 线圈个数 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 0F | 00 | 64 | 00 | 0A | 94 | 20 |
16功能码,写多个寄存器
例如,主站希望给02号从站的2000~2002号寄存器,分别写入下面的值:
| 2000 | 2001 | 2002 |
|---|---|---|
| +12345 | 0 | -12345 |
主站发送报文:
| 地址 | 功能码 | 寄存器起始地址 (高字节) | 寄存器起始地址 (低字节) | 寄存器个数 (高字节) | 寄存器个数 (低字节) | 字节数 | 数据内容 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | 数据内容 (高字节) | 数据内容 (低字节) | 数据内容 (高字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02 | 10 | 07 | D0 | 00 | 03 | 06 | 30 | 39 | 00 | 00 | CF | C7 | 70 | 09 |
从站收到后,发送报文给主站确认:
| 地址 | 功能码 | 寄存器起始地址 (高字节) | 寄存器起始地址 (低字节) | 寄存器个数 (高字节) | 寄存器个数 (低字节) | CRC校验 (高字节) | CRC校验 (低字节) |
|---|---|---|---|---|---|---|---|
| 02 | 10 | 07 | D0 | 00 | 03 | 80 | B6 |
Modbus RTU从站代码
Modbus RTU从站源码下载
首先在工程目录创建一个ModbusSlave文件夹,创建Modbus_Slave.c和Modbus_Slave.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}/ModbusSlave/Modbus_Slave.c ${CMAKE_SOURCE_DIR}/OLED/OLED.c)# Add include pathstarget_include_directories(${CMAKE_PROJECT_NAME} PRIVATE # Add user defined include paths ${CMAKE_SOURCE_DIR}/ModbusSlave ${CMAKE_SOURCE_DIR}/OLED) |
Modbus_Slave.c
1234567891011121314151617 | extern UART_HandleTypeDef huart3;extern TIM_HandleTypeDef htim2; uint8_t rx_buffer[RX_BUFFER_SIZE]; //接收缓冲区uint8_t rx_index = 0; //接收缓冲区索引uint32_t last_rx_time = 0; //上次接收数据的时间戳uint16_t Modbus_Register[5000];//寄存器数组,存储Modbus寄存器数据uint16_t Modbus_Coil[1000];//线圈数组,存储Modbus线圈数据 |
这里需要注意,Frame_Timeout_ms的时间由波特率决定,由于设定的波特率为9600,因此这里设置为4。
123456789101112131415161718192021 | //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) { // 1. 先关闭正在进行的接收中断 HAL_UART_AbortReceive_IT(&huart3); // 2. 处理已接收的帧 Modbus_Handle_Rx(rx_buffer, rx_index); // 3. 重置索引 rx_index = 0; // 4. 重新启动接收中断,从缓冲区头部开始 HAL_UART_Receive_IT(&huart3, &rx_buffer[0], 1); } } }} |
如果当前的时间和最后一次接到报文的时间大于超时时间(波特率9600的情况下为4ms),表明当前总线上处于空闲状态,即接收到了完整的一帧数据。
接收数据时,先确认接收缓冲区中确实有数据。如果有数据,先暂停接收中断,再去处理接收到的帧,最后再重新开启接收。
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); //重新开始接收 } }} |
和定时器中断进行配合,流程为:
启动接收中断 → 一个字节一个字节地进中断,记录时间 → 静默超时后,定时器中断中止接收,处理帧,重置索引 → 再次启动接收中断,循环往复。
12345 | void Modbus_Slave_Init(void){ HAL_TIM_Base_Start_IT(&htim2); HAL_UART_Receive_IT(&huart3, &rx_buffer[0], 1);} |
Modbus从站初始化,启动定时器中断和串口接收中断。
123456789101112 | //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_GPIO_WritePin(GPIOE, GPIO_PIN_3, GPIO_PIN_RESET);} |
Modbus发送函数,需要控制MAX485的RE和DE引脚。在发送数据之前,必须先将收发器置为发送模式,发送完成后恢复。发送数据时,RE和DE引脚悬空,接收数据时,RE和DE都接地。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 | //接收数据处理函数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校验失败,丢弃数据 } if(data[0] != Modbus_Slave_ID)//检查从站地址是否匹配,如果不匹配则丢弃数据 { return; } //解析从站返回的数据,根据功能码进行处理 uint8_t function_code = data[1]; //功能码 //根据功能码不同,进行不同的处理 if (function_code == 0x01) //功能码01处理 { //解析地址 uint16_t start_address = (data[2] << 8) | data[3]; //起始地址 //解析线圈数量 uint16_t num_coils = (data[4] << 8) | data[5]; //线圈数量 //调用01功能码处理函数 Modbus_Slave_01(num_coils, start_address); } else if (function_code == 0x03)//功能码03处理 { //解析地址 uint16_t start_address = (data[2] << 8) | data[3]; //起始地址 //解析寄存器数量 uint16_t num_registers = (data[4] << 8) | data[5]; //寄存器数量 //调用03功能码处理函数 Modbus_Slave_03(num_registers, start_address); } else if(function_code == 0x05) //功能码05处理 { //解析地址 uint16_t coil_address = (data[2] << 8) | data[3]; //线圈地址 //解析线圈值 uint8_t coil_value = (data[4] == 0xFF) ? 1 : 0; //线圈值,0xFF表示ON,0x00表示OFF //更新线圈状态 if (coil_value) { Modbus_Coil[coil_address / 16] |= (1 << (coil_address % 16)); //设置对应位为1 } else { Modbus_Coil[coil_address / 16] &= ~(1 << (coil_address % 16)); //设置对应位为0 } //调用05功能码处理函数 Modbus_Slave_05(coil_address, coil_value); } else if (function_code == 0x06) //功能码06处理 { //解析地址 uint16_t reg_address = (data[2] << 8) | data[3]; //寄存器地址 //解析寄存器值 uint16_t reg_value = (data[4] << 8) | data[5]; //寄存器值 //更新寄存器值 Modbus_Register[reg_address] = reg_value; //调用06功能码处理函数 Modbus_Slave_06(reg_address, reg_value); } else if (function_code == 0x0F) //功能码15处理 { //解析地址 uint16_t start_address = (data[2] << 8) | data[3]; //起始地址 //解析线圈数量 uint16_t num_coils = (data[4] << 8) | data[5]; //线圈数量 //更新线圈状态 for (uint16_t i = 0; i < num_coils; i++) { uint16_t coil_index = start_address + i; // 绝对线圈地址 uint16_t word_index = coil_index / 16; // 在Modbus_Coil数组中的索引(第几个uint16_t) uint8_t bit_position = coil_index % 16; // 在该uint16_t中的位位置(0-15) // 检查数据区中对应的位是否为ON if (data[7 + i / 8] & (1 << (i % 8))) { Modbus_Coil[word_index] |= (1 << bit_position); //设置对应位为1 } else { Modbus_Coil[word_index] &= ~(1 << bit_position); //设置对应位为0 } } Modbus_Slave_15(start_address, num_coils); } else if (function_code == 0x10) //功能码16处理 { //解析地址 uint16_t reg_address = (data[2] << 8) | data[3]; //寄存器地址 //解析寄存器数量 uint16_t num_registers = (data[4] << 8) | data[5]; //寄存器数量 //更新寄存器值 for (uint16_t i = 0; i < num_registers; i++) { Modbus_Register[reg_address + i] = (data[7 + i * 2] << 8) | data[8 + i * 2]; //更新寄存器值 } //调用16功能码处理函数 Modbus_Slave_16(reg_address, num_registers, (uint16_t *)&data[7]); }} |
接收数据的处理函数。根据不同的功能码分别处理,主要是保存寄存器/线圈的值。
处理数据之前,先进行CRC校验和从站号是否匹配,不正确直接丢弃。
1234567891011121314151617181920212223242526272829303132333435363738 | //01功能码,Modbus读取线圈状态void Modbus_Slave_01(uint16_t Num_Coils,uint16_t Start_Coils){ uint8_t TX_Frame[5 + (Num_Coils + 7) / 8]; //根据需要调整缓冲区大小 uint16_t crc; uint8_t byte_count = (Num_Coils + 7) / 8; TX_Frame[0] = Modbus_Slave_ID; //Modbus RTU从站地址 TX_Frame[1] = 0x01; //功能码01 TX_Frame[2] = (Num_Coils + 7) / 8; //字节数 // 初始化数据区为0 for (uint8_t i = 0; i < byte_count; i++) { TX_Frame[3 + i] = 0; } // 填充线圈状态,每个uint16_t存储16个线圈 for (uint16_t i = 0; i < Num_Coils; i++) { uint16_t coil_index = Start_Coils + i; // 绝对线圈地址 uint16_t word_index = coil_index / 16; // 在Modbus_Coil数组中的索引(第几个uint16_t) uint8_t bit_position = coil_index % 16; // 在该uint16_t中的位位置(0-15) // 检查该线圈是否为ON if (Modbus_Coil[word_index] & (1 << bit_position)) { // 在响应帧中设置对应位 TX_Frame[3 + i / 8] |= (1 << (i % 8)); } } crc = Modbus_CRC16(TX_Frame, 3 + (Num_Coils + 7) / 8); //计算CRC校验码 TX_Frame[3 + (Num_Coils + 7) / 8] = crc & 0xFF; //CRC低字节 TX_Frame[4 + (Num_Coils + 7) / 8] = (crc >> 8) & 0xFF; //CRC高字节 Modbus_Send(TX_Frame, 5 + (Num_Coils + 7) / 8);} |
功能码1的处理,主要是回复主站查询地址的线圈状态,并返回给主站。TX_Frame[2] = (Num_Coils + 7) / 8;由于Modbus报文一帧就是1个字,每8个线圈通过一个字(一帧报文)发送,每个线圈占一个二进制的位,因此一个字就是8个线圈(8个二进制位)。
比如查询9个连续的线圈,因为一帧只能保存8个二进制位,已经不够储存9个二进制位了,因此通过2帧发送。TX_Frame[2]就是2。
1234567891011121314151617181920 | //03功能码,Modbus读取保持寄存器void Modbus_Slave_03(uint16_t Num_Regs,uint16_t Start_Regs){ uint8_t TX_Frame[5 + Num_Regs * 2]; //根据需要调整缓冲区大小 uint16_t crc; TX_Frame[0] = Modbus_Slave_ID;//Modbus RTU从站地址 TX_Frame[1] = 0x03; TX_Frame[2] = Num_Regs * 2; for (uint16_t i = 0; i < Num_Regs; i++) { TX_Frame[3 + i * 2] = (Modbus_Register[Start_Regs + i] >> 8) & 0xFF; TX_Frame[4 + i * 2] = Modbus_Register[Start_Regs + i] & 0xFF; } crc = Modbus_CRC16(TX_Frame, 3 + 2 * Num_Regs); //计算CRC校验码 TX_Frame[3 + 2 * Num_Regs] = crc & 0xFF; //CRC低字节 TX_Frame[4 + 2 * Num_Regs] = (crc >> 8) & 0xFF; //CRC高字节 Modbus_Send(TX_Frame, 5 + 2 * Num_Regs);} |
功能码3的处理,这里TX_Frame[2] = Num_Regs * 2;是因为一个寄存器就是通过2帧发送的(一个寄存器就是一个字,两个字节,16个二进制位,而一帧只能发送8个二进制位,一个字节)
构建数据帧时,就是找到指定的寄存器,放到指定的位置。
12345678910111213141516171819 | //05功能码,Modbus写单个线圈void Modbus_Slave_05(uint16_t Coil_Address, uint8_t Coil_Value){ uint8_t TX_Frame[8]; //根据需要调整缓冲区大小 uint16_t crc; TX_Frame[0] = Modbus_Slave_ID; //Modbus RTU从站地址 TX_Frame[1] = 0x05; TX_Frame[2] = (Coil_Address >> 8) & 0xFF; //线圈地址高字节 TX_Frame[3] = Coil_Address & 0xFF; //线圈地址低字节 TX_Frame[4] = (Coil_Value == 0) ? 0x00 : 0xFF; //线圈值,0x00表示OFF,0xFF表示ON TX_Frame[5] = 0x00; //线圈值低字节,保持为0 crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码 TX_Frame[6] = crc & 0xFF; //CRC低字节 TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节 Modbus_Send(TX_Frame, 8);} |
05功能码的处理,这里会把发送的报文原样返回给主站。
如果线圈值是0,TX_Frame[4]和TX_Frame[5]都写0x00
如果线圈值是1,TX_Frame[4]写0xFF,TX_Frame[5]写0x00
因为在Modbus_Handle_Rx函数中已经把主站写线圈的数据放在了线圈数组中,所以这里等同于原样返回。
12345678910111213141516171819 | //06功能码,Modbus写单个保持寄存器void Modbus_Slave_06(uint16_t Reg_Address, uint16_t Reg_Value){ uint8_t TX_Frame[8]; //根据需要调整缓冲区大小 uint16_t crc; TX_Frame[0] = Modbus_Slave_ID;//Modbus RTU从站地址 TX_Frame[1] = 0x06; TX_Frame[2] = (Reg_Address >> 8) & 0xFF; //寄存器地址高字节 TX_Frame[3] = Reg_Address & 0xFF; //寄存器地址低字节 TX_Frame[4] = (Reg_Value >> 8) & 0xFF; //寄存器值高字节 TX_Frame[5] = Reg_Value & 0xFF; //寄存器值低字节 crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码 TX_Frame[6] = crc & 0xFF; //CRC低字节 TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节 Modbus_Send(TX_Frame, 8);} |
06功能码的处理,也是把主站发送的报文原样返回。
123456789101112131415161718 | //15功能码,Modbus写多个线圈void Modbus_Slave_15(uint16_t Coil_Address, uint16_t Num_Coils){ uint8_t TX_Frame[8]; //根据需要调整缓冲区大小 uint16_t crc; TX_Frame[0] = Modbus_Slave_ID; //Modbus RTU从站地址 TX_Frame[1] = 0x0F; //功能码15 TX_Frame[2] = (Coil_Address >> 8) & 0xFF; //线圈地址高字节 TX_Frame[3] = Coil_Address & 0xFF; //线圈地址低字节 TX_Frame[4] = (Num_Coils >> 8) & 0xFF; //线圈数量高字节 TX_Frame[5] = Num_Coils & 0xFF; //线圈数量低字节 crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码 TX_Frame[6] = crc & 0xFF; //CRC低字节 TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节 Modbus_Send(TX_Frame, 8);} |
15功能码,构建发送帧时不需要返回数据,只要返回起始地址,线圈数量即可。
12345678910111213141516171819 | //16功能码,Modbus写多个保持寄存器void Modbus_Slave_16(uint16_t Reg_Address, uint16_t Num_Regs, uint16_t *Reg_Values){ uint8_t TX_Frame[8]; //根据需要调整缓冲区大小 uint16_t crc; TX_Frame[0] = Modbus_Slave_ID; //Modbus RTU从站地址 TX_Frame[1] = 0x10; //功能码16 TX_Frame[2] = (Reg_Address >> 8) & 0xFF; //寄存器地址高字节 TX_Frame[3] = Reg_Address & 0xFF; //寄存器地址低字节 TX_Frame[4] = (Num_Regs >> 8) & 0xFF; //寄存器数量高字节 TX_Frame[5] = Num_Regs & 0xFF; //寄存器数量低字节 crc = Modbus_CRC16(TX_Frame, 6); //计算CRC校验码 TX_Frame[6] = crc & 0xFF; //CRC低字节 TX_Frame[7] = (crc >> 8) & 0xFF; //CRC高字节 Modbus_Send(TX_Frame, 8);} |
16功能码,也是返回寄存器起始地址和数量即可。
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值} |
CRC校验函数。