【秋招】嵌入式面试八股文 - Modbus 协议篇
【秋招】嵌入式面试八股文 - 最全专栏
1. Modbus基础概念
Q: 什么是Modbus协议?它有哪些特点?
答:Modbus是一种应用层通信协议,最初由Modicon(现为施耐德电气)开发,用于可编程逻辑控制器(PLC)之间的通信。其主要特点包括:
- 主从架构:采用主从(Master-Slave)通信模式,一个主设备可以控制多个从设备
- 开放标准:公开的协议规范,无需支付许可费用
- 简单可靠:协议结构简单,易于实现和调试
- 多种传输模式:支持RTU、ASCII和TCP等多种传输模式
- 广泛应用:在工业自动化、能源管理、楼宇自动化等领域广泛应用
- 多种物理层支持:可基于RS-232、RS-485、以太网等物理层实现
2. Modbus通信模式
Q: Modbus有哪几种通信模式?它们有什么区别?
答:Modbus主要有三种通信模式:
- Modbus RTU:使用二进制编码传输数据每个8位字节包含两个4位十六进制字符消息帧之间有静默间隔(至少3.5个字符时间)使用CRC-16校验优点:数据紧凑,传输效率高应用:基于RS-485/RS-232的串行通信
- Modbus ASCII:使用ASCII字符编码传输数据每个字节用两个ASCII字符表示消息以冒号(:)开始,以回车换行(CR/LF)结束使用LRC校验优点:可读性好,便于调试缺点:传输效率低于RTU模式应用:需要人工查看数据的场合
- Modbus TCP:基于TCP/IP协议使用MBAP(Modbus Application Protocol)头部无需校验和(由TCP协议保证数据完整性)优点:可通过以太网传输,支持更高速率和更长距离应用:工厂自动化网络、远程监控系统
// Modbus RTU帧格式
typedef struct {
uint8_t slave_address; // 从站地址(1-247)
uint8_t function_code; // 功能码
uint8_t data[253]; // 数据域(最大253字节)
uint16_t crc; // CRC校验
} ModbusRTU_Frame;
// Modbus TCP帧格式
typedef struct {
uint16_t transaction_id; // 事务标识符
uint16_t protocol_id; // 协议标识符(0=Modbus)
uint16_t length; // 后续字节数
uint8_t unit_id; // 单元标识符(相当于从站地址)
uint8_t function_code; // 功能码
uint8_t data[253]; // 数据域
} ModbusTCP_Frame;
3. Modbus功能码
Q: Modbus常用的功能码有哪些?它们的作用是什么?
答:Modbus常用功能码及其作用:
- 读取类功能码:01 (0x01): 读取线圈状态(Read Coil Status)用于读取离散输出(DO)的状态(单个位)可读取多个连续的线圈02 (0x02): 读取输入状态(Read Input Status)用于读取离散输入(DI)的状态(单个位)只读,不可写入03 (0x03): 读取保持寄存器(Read Holding Registers)用于读取可读写的16位寄存器通常用于存储设置值、控制参数等04 (0x04): 读取输入寄存器(Read Input Registers)用于读取只读的16位寄存器通常用于存储测量值、状态信息等
- 写入类功能码:05 (0x05): 写单个线圈(Write Single Coil)用于控制单个离散输出数据值为0x0000(关闭)或0xFF00(打开)06 (0x06): 写单个寄存器(Write Single Register)用于写入单个16位寄存器15 (0x0F): 写多个线圈(Write Multiple Coils)用于同时控制多个离散输出16 (0x10): 写多个寄存器(Write Multiple Registers)用于同时写入多个16位寄存器
- 诊断类功能码:07 (0x07): 读取异常状态(Read Exception Status)08 (0x08): 诊断(Diagnostics)17 (0x11): 报告从站ID(Report Slave ID)
// 功能码使用示例 - 读取保持寄存器
uint8_t read_holding_register(uint8_t slave_addr, uint16_t reg_addr, uint16_t reg_count, uint16_t *data) {
uint8_t send_buf[8];
uint8_t recv_buf[256];
// 构建请求帧
send_buf[0] = slave_addr; // 从站地址
send_buf[1] = 0x03; // 功能码:读取保持寄存器
send_buf[2] = reg_addr >> 8; // 寄存器地址高字节
send_buf[3] = reg_addr & 0xFF; // 寄存器地址低字节
send_buf[4] = reg_count >> 8; // 寄存器数量高字节
send_buf[5] = reg_count & 0xFF; // 寄存器数量低字节
// 计算CRC
uint16_t crc = ModbusCRC16(send_buf, 6);
send_buf[6] = crc & 0xFF; // CRC低字节
send_buf[7] = crc >> 8; // CRC高字节
// 发送请求并接收响应
// ...
// 解析响应
if (recv_buf[0] != slave_addr || recv_buf[1] != 0x03) {
return MODBUS_ERROR;
}
uint8_t byte_count = recv_buf[2];
for (int i = 0; i < reg_count; i++) {
data[i] = (recv_buf[3 + i*2] << 8) | recv_buf[4 + i*2];
}
return MODBUS_OK;
}
4. Modbus数据模型
Q: Modbus的数据模型是什么?四种数据类型有什么区别?
答:Modbus定义了四种主要的数据类型(表),每种类型对应不同的功能码:
- 线圈(Coils):1位(二进制)数据,可读可写对应功能码:01(读)、05(写单个)、15(写多个)地址范围:00001-09999典型应用:控制输出,如继电器、指示灯等
- 离散输入(Discrete Inputs):1位(二进制)数据,只读对应功能码:02(读)地址范围:10001-19999典型应用:状态输入,如开关状态、传感器触点等
- 输入寄存器(Input Registers):16位(字)数据,只读对应功能码:04(读)地址范围:30001-39999典型应用:测量值,如温度、压力、流量等
- 保持寄存器(Holding Registers):16位(字)数据,可读可写对应功能码:03(读)、06(写单个)、16(写多个)地址范围:40001-49999典型应用:设置值、控制参数等
// Modbus数据模型实现示例
typedef struct {
// 线圈 - 可读写的位数据
uint8_t coils[1000/8]; // 1000个线圈,每8个占用1字节
// 离散输入 - 只读的位数据
uint8_t discrete_inputs[1000/8]; // 1000个离散输入
// 输入寄存器 - 只读的字数据
uint16_t input_registers[1000]; // 1000个输入寄存器
// 保持寄存器 - 可读写的字数据
uint16_t holding_registers[1000]; // 1000个保持寄存器
} ModbusDataModel;
// 访问数据模型的函数
uint8_t get_coil_status(ModbusDataModel *model, uint16_t address) {
if (address >= 1000) return 0;
return (model->coils[address/8] >> (address%8)) & 0x01;
}
void set_coil_status(ModbusDataModel *model, uint16_t address, uint8_t status) {
if (address >= 1000) return;
if (status)
model->coils[address/8] |= (1 << (address%8));
else
model->coils[address/8] &= ~(1 << (address%8));
}
5. Modbus通信过程
Q: 描述一下Modbus的通信过程,主站和从站如何交互?
答:Modbus的通信过程遵循主从模式,基本流程如下:
- 请求-响应模式:主站发起请求,从站响应每个请求只能有一个响应主站负责超时检测和重试
- RTU/ASCII模式通信过程:主站构建请求帧(地址、功能码、数据、校验)主站发送请求帧到总线所有从站接收请求帧目标从站(地址匹配)处理请求从站构建响应帧从站发送响应帧主站接收并处理响应帧
- TCP模式通信过程:主站与从站建立TCP连接主站构建MBAP头部和PDU(功能码+数据)主站发送请求从站接收并处理请求从站构建响应从站发送响应主站接收并处理响应
- 异常处理:如果从站无法正常处理请求,会返回异常响应异常响应的功能码为原功能码+0x80异常响应包含异常码,指示错误类型
// Modbus主站请求示例
void modbus_master_request(uint8_t slave_addr, uint8_t function_code, uint16_t start_addr, uint16_t count) {
uint8_t request[256];
uint8_t req_len = 0;
// 构建请求帧头
request[req_len++] = slave_addr;
request[req_len++] = function_code;
request[req_len++] = start_addr >> 8;
request[req_len++] = start_addr & 0xFF;
request[req_len++] = count >> 8;
request[req_len++] = count & 0xFF;
// 添加CRC校验
uint16_t crc = ModbusCRC16(request, req_len);
request[req_len++] = crc & 0xFF;
request[req_len++] = crc >> 8;
// 发送请求
uart_send_data(request, req_len);
// 等待响应
uint8_t response[256];
uint8_t resp_len = 0;
if (wait_for_response(response, &resp_len, 1000)) { // 1000ms超时
// 处理响应
process_response(response, resp_len);
} else {
// 超时处理
handle_timeout(slave_addr, function_code);
}
}
// Modbus从站响应示例
void modbus_slave_process(uint8_t *request, uint8_t req_len) {
// 检查地址是否匹配
if (request[0] != slave_address && request[0] != 0) {
return; // 不是发给本机的请求
}
// 验证CRC
uint16_t received_crc = (request[req_len-1] << 8) | request[req_len-2];
uint16_t calculated_crc = ModbusCRC16(request, req_len-2);
if (received_crc != calculated_crc) {
return; // CRC错误
}
uint8_t function_code = request[1];
uint16_t start_addr = (request[2] << 8) | request[3];
uint16_t count = (request[4] << 8) | request[5];
// 根据功能码处理请求
switch (function_code) {
case 0x03: // 读保持寄存器
handle_read_holding_registers(request, req_len);
break;
case 0x06: // 写单个寄存器
handle_write_single_register(request, req_len);
break;
// 其他功能码处理...
default:
// 不支持的功能码,返回异常
send_exception_response(function_code, 0x01);
break;
}
}
6. Modbus校验机制
Q: Modbus的校验机制有哪些?如何实现CRC16校验?
答:Modbus的校验机制根据通信模式不同而不同:
- RTU模式校验:使用CRC-16(循环冗余校验)多项式:x^16 + x^15 + x^2 + 1(0xA001)初始值:0xFFFF校验范围:从站地址到数据字段的所有字节校验结果:两个字节,低字节在前,高字节在后
- ASCII模式校验:使用LRC(纵向冗余校验)计算方法:将所有字节相加,取二进制反码(补码)校验范围:从站地址到数据字段的所有字节校验结果:一个字节,用两个ASCII字符表示
- TCP模式:无需额外校验,依靠TCP协议的校验机制
CRC16校验算法实现:
// Modbus RTU CRC16校验算法
uint16_t ModbusCRC16(uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF; // 初始值
for (uint16_t i = 0; i < length; i++) {
crc ^= (uint16_t)data[i]; // 异或当前字节
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001; // 多项式0xA001
} else {
crc >>= 1;
}
}
}
return crc;
}
// Modbus ASCII LRC校验算法
uint8_t ModbusLRC(uint8_t *data, uint16_t length) {
uint8_t lrc = 0; // 初始值为0
for (uint16_t i = 0; i < length; i++) {
lrc += data[i]; // 累加所有字节
}
return (uint8_t)(-((int8_t)lrc)); // 取二进制反码
}
7. Modbus异常处理
Q: Modbus的异常处理机制是什么?常见的异常码有哪些?
答:Modbus的异常处理机制如下:
- 异常响应格式:功能码:原功能码+0x80(最高位置1)数据域:包含一个异常码例如:请求功能码0x03,异常响应功能码为0x83
- 常见异常码:0x01:非法功能码(Illegal Function)从站不支持请求的功能码0x02:非法数据地址(Illegal Data Address)请求的数据地址不存在或超出范围0x03:非法数据值(Illegal Data Value)请求的数据值不合法(如超出允许范围)0x04:从站设备故障(Slave Device Failure)从站在处理请求时发生内部错误0x05:确认(Acknowledge)从站接受请求,但需要较长时间处理0x06:从站设备忙(Slave Device Busy)从站正在处理长时间命令,请求稍后重试0x08:内存奇偶校验错误(Memory Parity Error)从站检测到扩展内存的奇偶校验错误0x0A:网关路径不可用(Gateway Path Unavailable)用于网关,表示网关无法路由请求0x0B:网关目标设备无响应(Gateway Target Device Failed to Respond)用于网关,表示目标设备无响应
- 异常处理实现:
// 异常响应生成函数
void send_exception_response(uint8_t function_code, uint8_t exception_code) {
uint8_t response[5];
response[0] = slave_address;
response[1] = function_code | 0x80; // 设置最高位
response[2] = exception_code;
// 计算CRC
uint16_t crc = ModbusCRC16(response, 3);
response[3] = crc & 0xFF;
response[4] = crc >> 8;
// 发送异常响应
uart_send_data(response, 5);
}
// 异常处理示例
void handle_read_holding_registers(uint8_t *request, uint8_t req_len) {
uint16_t start_addr = (request[2] << 8) | request[3];
uint16_t reg_count = (request[4] << 8) | request[5];
// 检查地址范围
if (start_addr + reg_count > MAX_HOLDING_REGISTERS) {
send_exception_response(0x03, 0x02); // 非法数据地址
return;
}
// 检查寄存器数量
if (reg_count == 0 || reg_count > 125) {
send_exception_response(0x03, 0x03); // 非法数据值
return;
}
// 正常处理...
}
8. Modbus超时和重试机制
Q: 如何在Modbus通信中实现超时检测和重试机制?
答:Modbus通信中的超时检测和重试机制实现:
- 超时检测:主站发送请求后启动计时器如果在预定时间内未收到响应,则判定为超时超时时间设置应考虑通信速率、网络延迟等因素
- 重试机制:发生超时后,可以重新发送请求设置最大重试次数,避免无限重试多次重试失败后,通知上层应用通信故障
- 实现方法:
#define MODBUS_TIMEOUT_MS 1000 // 超时时间1秒
#define MAX_RETRIES 3 // 最大重试次数
// 带重试的Modbus请求函数
bool modbus_request_with_retry(uint8_t slave_addr, uint8_t function_code,
uint16_t start_addr, uint16_t count, uint8_t *response, uint8_t *resp_len) {
uint8_t retries = 0;
bool success = false;
while (retries < MAX_RETRIES && !success) {
// 发送请求
send_modbus_request(slave_addr, function_code, start_addr, count);
// 等待响应,带超时
uint32_t start_time = get_system_time_ms();
while ((get_system_time_ms() - start_time) < MODBUS_TIMEOUT_MS) {
if (uart_data_available()) {
// 接收并处理响应
if (receive_modbus_response(response, resp_len)) {
success = true;
break;
}
}
// 短暂延时,避免CPU占用过高
delay_ms(1);
}
if (!success) {
retries++;
log_message("Modbus request timeout, retry %d/%d", retries, MAX_RETRIES);
}
}
if (!success) {
log_message("Modbus communication failed after %d retries", MAX_RETRIES);
}
return success;
}
- 超时时间优化: 根据通信速率计算理论响应时间考虑网络延迟和从站处理时间可以实现自适应超时机制
// 自适应超时计算
uint32_t calculate_timeout(uint8_t function_code, uint16_t data_count) {
// 基本超时时间
uint32_t base_timeout = 100; // 100ms基础时间
// 根据功能码和数据量调整超时时间
switch (function_code) {
case 0x01: // 读线圈
case 0x02: // 读离散输入
return base_timeout + data_count * 1; // 每个位增加1ms
case 0x03: // 读保持寄存器
case 0x04: // 读输入寄存器
return base_timeout + data_count * 2; // 每个寄存器增加2ms
case 0x0F: // 写多个线圈
case 0x10: // 写多个寄存器
return base_timeout + data_count * 5; // 写操作需要更多时间
default:
return base_timeout + 500; // 其他功能码使用较长超时
}
}
9. Modbus TCP特性
Q: Modbus TCP与Modbus RTU有什么区别?如何处理Modbus TCP的网络故障?
答:Modbus TCP与Modbus RTU的区别及网络故障处理:
- 主要区别:帧格式:RTU:使用从站地址、功能码、数据、CRC校验TCP:使用MBAP头部(事务ID、协议ID、长度、单元ID)、功能码、数据传输媒介:RTU:通常基于RS-485/RS-232串行通信TCP:基于以太网TCP/IP通信校验机制:RTU:使用CRC-16校验TCP:依靠TCP协议的校验机制,无需额外校验寻址方式:RTU:使用从站地址
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【秋招】嵌入式八股文最全总结 文章被收录于专栏
双非本,211硕。本硕均为机械工程,自学嵌入式,在校招过程中拿到小米、格力、美的、比亚迪、海信、海康、大华、江波龙等offer。八股文本质是需要大家理解,因此里面的内容一定要详细、深刻!这个专栏是我个人的学习笔记总结,是对很多面试问题进行的知识点分析,专栏保证高质量,让大家可以高效率理解与吸收里面的知识点!掌握这里面的知识,面试绝对无障碍!


