FPGA 实现 Modbus 通信主要用于高速、并行处理场景,能够实现实时的数据交换和低延迟的响应。
1. Modbus 协议基础
Modbus 协议一般有两个主要版本:
- Modbus RTU:基于串行通信(RS-232/RS-485)。它使用二进制格式传输数据,通常适用于嵌入式系统中。
- Modbus TCP:基于以太网通信,采用 TCP/IP 协议栈进行数据传输。
Modbus RTU 帧格式:
- 设备地址(1 字节):唯一标识 Modbus 网络中的设备。
- 功能码(1 字节):定义主机请求的操作,如读/写寄存器。查询寄存器上的数据用
03,
修改寄存器的值就用06
- 数据域(可变):具体的操作数据,如寄存器地址和数量。
- CRC 校验(2 字节):用于检测传输过程中的错误。
FPGA 主要用于 Modbus RTU 的实现,因为其串行通信(RS-232 或 RS-485)特别适合在 FPGA 上使用硬件模块来处理。
Modbus是主从方式通信,不能同步进行通信,总线上每次只有一个数据进行传输。
功能码06示例:
主机发送: 01 06 0001 0007 99c8
从机回复: 01 06 0001 0007 99c8
分别为:地址、功能码、修改0001寄存器的值为0007、CRC
回复一致则写入正确
同时reg_03_01_o的值变为7
功能码03示例:
主机发送
01_03_0001_0001_d5ca
分别为:从机地址、03功能码、读取寄存器地址、读取的数据个数、CRC校验码
从机回复
01_03_01_0007_0986
分别为:地址、功能码、读取的数据个数、寄存器值为0007(通过06功能码写入的)、CRC
功能码04示例:
主机发送: 01 04 0001 0004 a009
地址_功能码_起始寄存器地址_读4个_crc
从机回复: 01 04 04 5347 7414 2021 0402 e15c
地址_功能码_数据个数_4个读出值_crc
读出值分别为read_04_01、read_04_02、read_04_03、read_04_04的值
主机发送: 01 04 0001 0003 e1cb
从机回复: 01 04 03 5347 7414 2021 0fd3
读出值分别为read_04_01、read_04_02、read_04_03的值
主机发送: 01 04 0002 0001 900a
从机回复: 01 04 01 7414 6e3f
读出read_04_02的值
主机发送: ff 04 0002 0001 85d4
从机回复: ff 04 01 7414 47eb
读出read_04_02的值
主机发送: 01 04 0003 0003 400b
从机回复:01 04 03 2021 0402 7721 c9ec
读出read_04_03 、read_04_04 、read_04_05的值
主机发送: 01 04 0009 0005 e00b
从机回复: 01 84 03 01 00
09+05 >所有的寄存器数量,04 功能码下的异常,读取数据个数不对,illegal quantity return 03
功能码0x10:修改连续多个寄存器
主机发送起始地址
+寄存器个数
+总字节数
+数据
,从机返回起始地址
+寄存器数量
2. Modbus 在 FPGA 上的实现
1.uart_byte_tx
`timescale 1ns / 1ns
`define UD #1
module uart_byte_tx #
(
parameter CLK_FREQ = 'd50000000,
parameter BAUD_RATE = 'd9600
)
(
input clk ,
input rst_n ,
input tx_start, // start transfer with pos edge
input [7:0] tx_data , // data need to transfer
output reg tx_state, // sending duration
output reg tx_done , // pos-pulse for 1 tick indicates 1 byte transfer done
output reg rs232_tx // uart transfer pin
);
localparam START_BIT = 1'b0;
localparam STOP_BIT = 1'b1;
localparam BPS_PARAM = CLK_FREQ/BAUD_RATE;
//start 8bit_data transfer operation
reg tx_start_r0;
reg tx_start_r1;
wire tx_start_pos;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
tx_start_r0 <= `UD 1'b0;
tx_start_r1 <= `UD 1'b0;
end
else
begin
tx_start_r0 <= `UD tx_start;
tx_start_r1 <= `UD tx_start_r0;
end
end
assign tx_start_pos = ~tx_start_r1 & tx_start_r0;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
tx_state <= `UD 1'b0;
end
else
begin
if(tx_start_pos)
begin
tx_state <= `UD 1'b1;
end
else if(tx_done)
begin
tx_state <= `UD 1'b0;
end
else
begin
tx_state <= `UD tx_state;
end
end
end
reg [15:0] baud_rate_cnt;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
baud_rate_cnt <= `UD 16'd0;
end
else
begin
if(tx_state)
begin
if(baud_rate_cnt >= BPS_PARAM - 1)
begin
baud_rate_cnt <= `UD 16'd0;
end
else
begin
baud_rate_cnt <= `UD baud_rate_cnt + 1'b1;
end
end
else
begin
baud_rate_cnt <= `UD 16'd0;
end
end
end
// generate bps_clk signal
reg bps_clk;
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
bps_clk <= `UD 1'b0;
end
else
begin
if(baud_rate_cnt == 16'd1 )
begin
bps_clk <= `UD 1'b1;
end
else
begin
bps_clk <= `UD 1'b0;
end
end
end
//bps counter
reg [3:0] bps_cnt;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
bps_cnt <= `UD 4'd0;
end
else
begin
if(bps_cnt == 4'd11)
begin
bps_cnt <= `UD 4'd0;
end
else if(bps_clk)
begin
bps_cnt <= `UD bps_cnt + 1'b1;
end
else
begin
bps_cnt <= `UD bps_cnt;
end
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
tx_done <= `UD 1'b0;
end
else if(bps_cnt == 4'd11)
begin
tx_done <= `UD 1'b1;
end
else
begin
tx_done <= `UD 1'b0;
end
end
reg [7:0] tx_data_r;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
tx_data_r <= `UD 8'b0;
end
else
begin
if(tx_start_pos)
begin
tx_data_r <= `UD tx_data;
end
else
begin
tx_data_r <= `UD tx_data_r;
end
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
rs232_tx <= `UD 1'b1;
end
else begin
case(bps_cnt)
0:rs232_tx <= `UD 1'b1; // idle hi
1:rs232_tx <= `UD START_BIT; // start bit lo
2:rs232_tx <= `UD tx_data_r[0]; // LSB first
3:rs232_tx <= `UD tx_data_r[1]; //
4:rs232_tx <= `UD tx_data_r[2]; //
5:rs232_tx <= `UD tx_data_r[3]; //
6:rs232_tx <= `UD tx_data_r[4]; //
7:rs232_tx <= `UD tx_data_r[5]; //
8:rs232_tx <= `UD tx_data_r[6]; //
9:rs232_tx <= `UD tx_data_r[7]; // MSB last
// No parity
10:rs232_tx <= `UD STOP_BIT; // stop bit hi
default:rs232_tx <= `UD 1'b1; // idle hi
endcase
end
end
endmodule
2.1.uart_byte_rx
`timescale 1ns / 1ns
`define UD #1
module uart_byte_rx #
(
parameter CLK_FREQ = 'd50000000,
parameter BAUD_RATE = 'd9600
)
(
input clk ,
input rst_n ,
output reg [7:0] rx_data , // data need to transfer
output reg rx_state, // recieving duration
output reg rx_done , // pos-pulse for 1 tick indicates 1 byte transfer done
input rs232_rx // uart transfer pin
);
localparam BPS_PARAM = (CLK_FREQ/BAUD_RATE)>>4; // oversample by x16
// sample sum registers, sum of 6 samples
reg [2:0] rx_data_r [0:7];
reg [2:0] START_BIT;
reg [2:0] STOP_BIT;
reg [7:0] bps_cnt;
reg rs232_rx0,rs232_rx1,rs232_rx2;
//Detect negedge of rs232_rx
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rs232_rx0 <= `UD 1'b0;
rs232_rx1 <= `UD 1'b0;
rs232_rx2 <= `UD 1'b0;
end else begin
rs232_rx0 <= `UD rs232_rx;
rs232_rx1 <= `UD rs232_rx0;
rs232_rx2 <= `UD rs232_rx1;
end
end
wire neg_rs232_rx;
assign neg_rs232_rx = rs232_rx2 & ~rs232_rx1;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
rx_state <= `UD 1'b0;
end
else
begin
if(neg_rs232_rx)
begin
rx_state <= `UD 1'b1;
end
else if(rx_done || (bps_cnt == 8'd12 && (START_BIT > 2))) // at least 3 of 6 samples are 1, START_BIT not ok
begin
rx_state <= `UD 1'b0;
end
else
begin
rx_state <= `UD rx_state;
end
end
end
reg [15:0] baud_rate_cnt;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
baud_rate_cnt <= `UD 16'd0;
end
else
begin
if(rx_state)
begin
if(baud_rate_cnt >= BPS_PARAM - 1)
begin
baud_rate_cnt <= `UD 16'd0;
end
else
begin
baud_rate_cnt <= `UD baud_rate_cnt + 1'b1;
end
end
else
begin
baud_rate_cnt <= `UD 16'd0;
end
end
end
// generate bps_clk signal
reg bps_clk;
always @ (posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
bps_clk <= `UD 1'b0;
end
else
begin
if(baud_rate_cnt == BPS_PARAM>>1 )//sample in cnt center
begin
bps_clk <= `UD 1'b1;
end
else
begin
bps_clk <= `UD 1'b0;
end
end
end
//bps counter
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
bps_cnt <= `UD 8'd0;
end
else
begin
if(bps_cnt == 8'd159 || (bps_cnt == 8'd12 && (START_BIT > 2)))
begin
bps_cnt <= `UD 8'd0;
end
else if(baud_rate_cnt >= BPS_PARAM - 1'b1 )
begin
bps_cnt <= `UD bps_cnt + 1'b1;
end
else
begin
bps_cnt <= `UD bps_cnt;
end
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
rx_done <= `UD 1'b0;
end
else
begin
if(bps_cnt == 8'd159)
begin
rx_done <= `UD 1'b1;
end
else
begin
rx_done <= `UD 1'b0;
end
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
START_BIT <= `UD 3'd0;
rx_data_r[0] <= `UD 3'd0;
rx_data_r[1] <= `UD 3'd0;
rx_data_r[2] <= `UD 3'd0;
rx_data_r[3] <= `UD 3'd0;
rx_data_r[4] <= `UD 3'd0;
rx_data_r[5] <= `UD 3'd0;
rx_data_r[6] <= `UD 3'd0;
rx_data_r[7] <= `UD 3'd0;
STOP_BIT <= `UD 3'd0;
end
else
begin
if(bps_clk)
begin
case(bps_cnt)
0:
begin
START_BIT <= `UD 3'd0;
rx_data_r[0] <= `UD 3'd0;
rx_data_r[1] <= `UD 3'd0;
rx_data_r[2] <= `UD 3'd0;
rx_data_r[3] <= `UD 3'd0;
rx_data_r[4] <= `UD 3'd0;
rx_data_r[5] <= `UD 3'd0;
rx_data_r[6] <= `UD 3'd0;
rx_data_r[7] <= `UD 3'd0;
STOP_BIT <= `UD 3'd0;
end
// 160 bps_cnt from 0 to 159 in a byte, each bit has 16 bps_clk (0~15),
// sample form 6 to 11 is the middle 6 of 16 bps_clk
6,7,8,9,10,11:
begin
START_BIT <= `UD START_BIT + rs232_rx2;
end
22,23,24,25,26,27:
begin
rx_data_r[0] <= `UD rx_data_r[0] + rs232_rx2;
end
38,39,40,41,42,43:
begin
rx_data_r[1] <= `UD rx_data_r[1] + rs232_rx2;
end
54,55,56,57,58,59:
begin
rx_data_r[2] <= `UD rx_data_r[2] + rs232_rx2;
end
70,71,72,73,74,75:
begin
rx_data_r[3] <= `UD rx_data_r[3] + rs232_rx2;
end
86,87,88,89,90,91:
begin
rx_data_r[4] <= `UD rx_data_r[4] + rs232_rx2;
end
102,103,104,105,106,107:
begin
rx_data_r[5] <= `UD rx_data_r[5] + rs232_rx2;
end
118,119,120,121,122,123:
begin
rx_data_r[6] <= `UD rx_data_r[6] + rs232_rx2;
end
134,135,136,137,138,139:
begin
rx_data_r[7] <= `UD rx_data_r[7] + rs232_rx2;
end
150,151,152,153,154,155:
begin
STOP_BIT <= `UD STOP_BIT + rs232_rx2;
end
default:
begin
START_BIT <= `UD START_BIT;
rx_data_r[0] <= `UD rx_data_r[0];
rx_data_r[1] <= `UD rx_data_r[1];
rx_data_r[2] <= `UD rx_data_r[2];
rx_data_r[3] <= `UD rx_data_r[3];
rx_data_r[4] <= `UD rx_data_r[4];
rx_data_r[5] <= `UD rx_data_r[5];
rx_data_r[6] <= `UD rx_data_r[6];
rx_data_r[7] <= `UD rx_data_r[7];
STOP_BIT <= `UD STOP_BIT;
end
endcase
end
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
rx_data <= `UD 8'd0;
end
else
begin
if(bps_cnt == 8'd159)
begin
// rx_data_r[x] has 3 width, actual sample sum range 0~6, rx_data_r[x][2] means sum/4
// if sum >=4 sample value == 1, else sum < 4 sample value == 0
rx_data[0] <= `UD rx_data_r[0][2];
rx_data[1] <= `UD rx_data_r[1][2];
rx_data[2] <= `UD rx_data_r[2][2];
rx_data[3] <= `UD rx_data_r[3][2];
rx_data[4] <= `UD rx_data_r[4][2];
rx_data[5] <= `UD rx_data_r[5][2];
rx_data[6] <= `UD rx_data_r[6][2];
rx_data[7] <= `UD rx_data_r[7][2];
end
end
end
endmodule
3.ct_15_gen
`timescale 1ns / 1ns
`define UD #1
module ct_15t_gen #
(
parameter CLK_FREQ = 'd50000000,// 50MHz
parameter BAUD_RATE = 'd9600
)
(
input clk ,
input rst_n ,
input rx_done , //表示接收端接收到一个完整字节的信号,触发一次有效脉冲
input rx_state , //表示当前处于接收状态
output rx_drop_frame // if intervel >1.5T == 1,表明丢帧情况
);
localparam BPS_PARAM = (CLK_FREQ/BAUD_RATE); //计算得到的分频系数,表示每位传输所需的时钟周期数