8 自定义IP核

前面学过AXI GPIO,这个实验使用的是赛灵斯官方的IP核去实现的功能。赛灵斯官方提供了很多的IP核,比如AXI DMI、AXI VDMI等等,但这些IP核往往是针对通用的使用场景,并不能事完全适应各种情况,那这时候就需要程序员去自己编写Verilog代码,封装成自己的IP核加入到Block Design里面,这个就叫自定义的IP核。

ZYNQ包含PS和PL,PS跟PL是怎么通信的呢——通过AXI接口通信对吧,所以说自己在实现功能的时候,难免是需要写AXI的接口协议,而这个Vivado软件提供了一种封装IP核的形式,它可以封装带AXI接口的一个IP核,简单说程序员只专注实现核心功能,利用AXI进行PS和PL通信部分软件能自动生成。

8.1 自定义IP核概述

自定义IP核是指在Vivado软件环境下,借助创建和封装IP向导的功能,把用户按照特定需求所设计的硬件模块封装成IP核的形式。

上图是正点原子创建的IP核,首先创建了一个带AXI接口的IP核,然后进行修改,最终实现PWM功能。

自定义IP核的官方参考文档:UG1118: User Guide: Creating and Packaging Custom IP

下面看一下Block Design支持的输入类型:

8.2 实验任务

本章的实验任务是通过自定义一个LED IP核,来控制PL LED呈现呼吸灯的效果,并且PS可以通过AXI接口控制呼吸灯的开关和呼吸的频率。

其实这个实现思路也比较简单,通过Vivado软件创建一个带AXI接口的IP核,因为本实验中PS与PL交互的数据很少,所以可以创建一个AXI4-Light接口的IP核。创建好之后,在这个代码的基础上添加呼吸灯的功能,像前面的FPGA设计里面也讲解了呼吸灯的实现,所以说可以把它们融合到一起,就可以实现实验任务了。然后PS制呼吸灯的开关和频率,所以PS代码就通过Vitis软件编写C代码实现。这就是设计思路。

8.3 实验系统框图

8.4 硬件设计

首先,打开Vivado软件,之前打开软件之后都是创建一个新的工程,而本次是去管理IP核,所以本次点击选项manager ip:

点击完成后,进入初始界面,选择“创建和打包一个新的IP”:

下面会弹出设置页面:

完成后,选择编辑IP核:

此时,会打开一个新的Vivado工程,在这个工程里面就可以对这个IP核进行修改。

  • breath_led_ip_v1_0文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
`timescale 1 ns / 1 ps

module breath_led_ip_v1_0 #
(
// Users to add parameters here
parameter START_FREQ_STEP = 10'd1,//设置频率步长初始值
// User parameters ends
// Do not modify the parameters beyond this line


// Parameters of Axi Slave Bus Interface S0_AXI
parameter integer C_S0_AXI_DATA_WIDTH = 32,
parameter integer C_S0_AXI_ADDR_WIDTH = 4
)
(
// Users to add ports here
output led,
// User ports ends
// Do not modify the ports beyond this line


// Ports of Axi Slave Bus Interface S0_AXI
input wire s0_axi_aclk,
input wire s0_axi_aresetn,
input wire [C_S0_AXI_ADDR_WIDTH-1 : 0] s0_axi_awaddr,
input wire [2 : 0] s0_axi_awprot,
input wire s0_axi_awvalid,
output wire s0_axi_awready,
input wire [C_S0_AXI_DATA_WIDTH-1 : 0] s0_axi_wdata,
input wire [(C_S0_AXI_DATA_WIDTH/8)-1 : 0] s0_axi_wstrb,
input wire s0_axi_wvalid,
output wire s0_axi_wready,
output wire [1 : 0] s0_axi_bresp,
output wire s0_axi_bvalid,
input wire s0_axi_bready,
input wire [C_S0_AXI_ADDR_WIDTH-1 : 0] s0_axi_araddr,
input wire [2 : 0] s0_axi_arprot,
input wire s0_axi_arvalid,
output wire s0_axi_arready,
output wire [C_S0_AXI_DATA_WIDTH-1 : 0] s0_axi_rdata,
output wire [1 : 0] s0_axi_rresp,
output wire s0_axi_rvalid,
input wire s0_axi_rready
);
// Instantiation of Axi Bus Interface S0_AXI
breath_led_ip_v1_0_S0_AXI # (
.START_FREQ_STEP(START_FREQ_STEP),
.C_S_AXI_DATA_WIDTH(C_S0_AXI_DATA_WIDTH),
.C_S_AXI_ADDR_WIDTH(C_S0_AXI_ADDR_WIDTH)
) breath_led_ip_v1_0_S0_AXI_inst (
.led(led),
.S_AXI_ACLK(s0_axi_aclk),
.S_AXI_ARESETN(s0_axi_aresetn),
.S_AXI_AWADDR(s0_axi_awaddr),
.S_AXI_AWPROT(s0_axi_awprot),
.S_AXI_AWVALID(s0_axi_awvalid),
.S_AXI_AWREADY(s0_axi_awready),
.S_AXI_WDATA(s0_axi_wdata),
.S_AXI_WSTRB(s0_axi_wstrb),
.S_AXI_WVALID(s0_axi_wvalid),
.S_AXI_WREADY(s0_axi_wready),
.S_AXI_BRESP(s0_axi_bresp),
.S_AXI_BVALID(s0_axi_bvalid),
.S_AXI_BREADY(s0_axi_bready),
.S_AXI_ARADDR(s0_axi_araddr),
.S_AXI_ARPROT(s0_axi_arprot),
.S_AXI_ARVALID(s0_axi_arvalid),
.S_AXI_ARREADY(s0_axi_arready),
.S_AXI_RDATA(s0_axi_rdata),
.S_AXI_RRESP(s0_axi_rresp),
.S_AXI_RVALID(s0_axi_rvalid),
.S_AXI_RREADY(s0_axi_rready)
);

// Add user logic here

// User logic ends

endmodule
  • breath_led_ip_v1_0_S0_AXI.v文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414

`timescale 1 ns / 1 ps

module breath_led_ip_v1_0_S0_AXI #
(
// Users to add parameters here
parameter START_FREQ_STEP = 10'd1, //设置频率步长初始值
// User parameters ends
// Do not modify the parameters beyond this line

// Width of S_AXI data bus
parameter integer C_S_AXI_DATA_WIDTH = 32,
// Width of S_AXI address bus
parameter integer C_S_AXI_ADDR_WIDTH = 4
)
(
// Users to add ports here
output led ,//LED
// User ports ends
// Do not modify the ports beyond this line

// Global Clock Signal
input wire S_AXI_ACLK,
// Global Reset Signal. This Signal is Active LOW
input wire S_AXI_ARESETN,
// Write address (issued by master, acceped by Slave)
input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR,
// Write channel Protection type. This signal indicates the
// privilege and security level of the transaction, and whether
// the transaction is a data access or an instruction access.
input wire [2 : 0] S_AXI_AWPROT,
// Write address valid. This signal indicates that the master signaling
// valid write address and control information.
input wire S_AXI_AWVALID,
// Write address ready. This signal indicates that the slave is ready
// to accept an address and associated control signals.
output wire S_AXI_AWREADY,
// Write data (issued by master, acceped by Slave)
input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA,
// Write strobes. This signal indicates which byte lanes hold
// valid data. There is one write strobe bit for each eight
// bits of the write data bus.
input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB,
// Write valid. This signal indicates that valid write
// data and strobes are available.
input wire S_AXI_WVALID,
// Write ready. This signal indicates that the slave
// can accept the write data.
output wire S_AXI_WREADY,
// Write response. This signal indicates the status
// of the write transaction.
output wire [1 : 0] S_AXI_BRESP,
// Write response valid. This signal indicates that the channel
// is signaling a valid write response.
output wire S_AXI_BVALID,
// Response ready. This signal indicates that the master
// can accept a write response.
input wire S_AXI_BREADY,
// Read address (issued by master, acceped by Slave)
input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR,
// Protection type. This signal indicates the privilege
// and security level of the transaction, and whether the
// transaction is a data access or an instruction access.
input wire [2 : 0] S_AXI_ARPROT,
// Read address valid. This signal indicates that the channel
// is signaling valid read address and control information.
input wire S_AXI_ARVALID,
// Read address ready. This signal indicates that the slave is
// ready to accept an address and associated control signals.
output wire S_AXI_ARREADY,
// Read data (issued by slave)
output wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA,
// Read response. This signal indicates the status of the
// read transfer.
output wire [1 : 0] S_AXI_RRESP,
// Read valid. This signal indicates that the channel is
// signaling the required read data.
output wire S_AXI_RVALID,
// Read ready. This signal indicates that the master can
// accept the read data and response information.
input wire S_AXI_RREADY
);

// AXI4LITE signals
reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_awaddr;
reg axi_awready;
reg axi_wready;
reg [1 : 0] axi_bresp;
reg axi_bvalid;
reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_araddr;
reg axi_arready;
reg [C_S_AXI_DATA_WIDTH-1 : 0] axi_rdata;
reg [1 : 0] axi_rresp;
reg axi_rvalid;

// Example-specific design signals
// local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
// ADDR_LSB is used for addressing 32/64 bit registers/memories
// ADDR_LSB = 2 for 32 bits (n downto 2)
// ADDR_LSB = 3 for 64 bits (n downto 3)
localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
localparam integer OPT_MEM_ADDR_BITS = 1;
//----------------------------------------------
//-- Signals for user logic register space example
//------------------------------------------------
//-- Number of Slave Registers 4
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
wire slv_reg_rden;
wire slv_reg_wren;
reg [C_S_AXI_DATA_WIDTH-1:0] reg_data_out;
integer byte_index;
reg aw_en;

// I/O Connections assignments

assign S_AXI_AWREADY = axi_awready;
assign S_AXI_WREADY = axi_wready;
assign S_AXI_BRESP = axi_bresp;
assign S_AXI_BVALID = axi_bvalid;
assign S_AXI_ARREADY = axi_arready;
assign S_AXI_RDATA = axi_rdata;
assign S_AXI_RRESP = axi_rresp;
assign S_AXI_RVALID = axi_rvalid;
// Implement axi_awready generation
// axi_awready is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_awready is
// de-asserted when reset is low.

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_awready <= 1'b0;
aw_en <= 1'b1;
end
else
begin
if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
begin
// slave is ready to accept write address when
// there is a valid write address and write data
// on the write address and data bus. This design
// expects no outstanding transactions.
axi_awready <= 1'b1;
aw_en <= 1'b0;
end
else if (S_AXI_BREADY && axi_bvalid)
begin
aw_en <= 1'b1;
axi_awready <= 1'b0;
end
else
begin
axi_awready <= 1'b0;
end
end
end

// Implement axi_awaddr latching
// This process is used to latch the address when both
// S_AXI_AWVALID and S_AXI_WVALID are valid.

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_awaddr <= 0;
end
else
begin
if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
begin
// Write Address latching
axi_awaddr <= S_AXI_AWADDR;
end
end
end

// Implement axi_wready generation
// axi_wready is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_wready is
// de-asserted when reset is low.

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_wready <= 1'b0;
end
else
begin
if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en )
begin
// slave is ready to accept write data when
// there is a valid write address and write data
// on the write address and data bus. This design
// expects no outstanding transactions.
axi_wready <= 1'b1;
end
else
begin
axi_wready <= 1'b0;
end
end
end

// Implement memory mapped register select and write logic generation
// The write data is accepted and written to memory mapped registers when
// axi_awready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted. Write strobes are used to
// select byte enables of slave registers while writing.
// These registers are cleared when reset (active low) is applied.
// Slave register write enable is asserted when valid address and data are available
// and the slave is ready to accept the write address and write data.
assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
slv_reg0 <= 0;
slv_reg1 <= 0;
slv_reg2 <= 0;
slv_reg3 <= 0;
end
else begin
if (slv_reg_wren)
begin
case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
2'h0:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 0
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
2'h1:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 1
slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
2'h2:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 2
slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
2'h3:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
// Respective byte enables are asserted as per write strobes
// Slave register 3
slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
default : begin
slv_reg0 <= slv_reg0;
slv_reg1 <= slv_reg1;
slv_reg2 <= slv_reg2;
slv_reg3 <= slv_reg3;
end
endcase
end
end
end

// Implement write response logic generation
// The write response and response valid signals are asserted by the slave
// when axi_wready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted.
// This marks the acceptance of address and indicates the status of
// write transaction.

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_bvalid <= 0;
axi_bresp <= 2'b0;
end
else
begin
if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID)
begin
// indicates a valid write response is available
axi_bvalid <= 1'b1;
axi_bresp <= 2'b0; // 'OKAY' response
end // work error responses in future
else
begin
if (S_AXI_BREADY && axi_bvalid)
//check if bready is asserted while bvalid is high)
//(there is a possibility that bready is always asserted high)
begin
axi_bvalid <= 1'b0;
end
end
end
end

// Implement axi_arready generation
// axi_arready is asserted for one S_AXI_ACLK clock cycle when
// S_AXI_ARVALID is asserted. axi_awready is
// de-asserted when reset (active low) is asserted.
// The read address is also latched when S_AXI_ARVALID is
// asserted. axi_araddr is reset to zero on reset assertion.

always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_arready <= 1'b0;
axi_araddr <= 32'b0;
end
else
begin
if (~axi_arready && S_AXI_ARVALID)
begin
// indicates that the slave has acceped the valid read address
axi_arready <= 1'b1;
// Read address latching
axi_araddr <= S_AXI_ARADDR;
end
else
begin
axi_arready <= 1'b0;
end
end
end

// Implement axi_arvalid generation
// axi_rvalid is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_ARVALID and axi_arready are asserted. The slave registers
// data are available on the axi_rdata bus at this instance. The
// assertion of axi_rvalid marks the validity of read data on the
// bus and axi_rresp indicates the status of read transaction.axi_rvalid
// is deasserted on reset (active low). axi_rresp and axi_rdata are
// cleared to zero on reset (active low).
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_rvalid <= 0;
axi_rresp <= 0;
end
else
begin
if (axi_arready && S_AXI_ARVALID && ~axi_rvalid)
begin
// Valid read data is available at the read data bus
axi_rvalid <= 1'b1;
axi_rresp <= 2'b0; // 'OKAY' response
end
else if (axi_rvalid && S_AXI_RREADY)
begin
// Read data is accepted by the master
axi_rvalid <= 1'b0;
end
end
end

// Implement memory mapped register select and read logic generation
// Slave register read enable is asserted when valid address is available
// and the slave is ready to accept the read address.
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
always @(*)
begin
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
2'h0 : reg_data_out <= slv_reg0;
2'h1 : reg_data_out <= slv_reg1;
2'h2 : reg_data_out <= slv_reg2;
2'h3 : reg_data_out <= slv_reg3;
default : reg_data_out <= 0;
endcase
end

// Output register or memory read data
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_rdata <= 0;
end
else
begin
// When there is a valid read address (S_AXI_ARVALID) with
// acceptance of read address by the slave (axi_arready),
// output the read dada
if (slv_reg_rden)
begin
axi_rdata <= reg_data_out; // register read data
end
end
end

// Add user logic here
breath_led #(
.START_FREQ_STEP (START_FREQ_STEP)
)
u_breath_led(
.sys_clk (S_AXI_ACLK),
.sys_rst_n (S_AXI_ARESETN),
.sw_ctrl (slv_reg0[0]),
.set_en (slv_reg1[31]),
.set_freq_step (slv_reg1[9:0]),
.led (led)
);
// User logic ends

endmodule
  • breath_led.v自己编写的呼吸灯文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
module breath_led(
input sys_clk , //系统时钟 50MHz
input sys_rst_n , //系统复位,低电平有效
input sw_ctrl , //呼吸灯开关控制信号 1:亮 0:灭
input set_en , //设置呼吸灯频率设置使能信号
input [9:0] set_freq_step , //设置呼吸灯频率变化步长

output led //LED灯
);

//parameter define
parameter START_FREQ_STEP = 10'd1; //设置频率步长初始值
parameter CNT_2US_MAX = 7'd100;
parameter CNT_2MS_MAX = 10'd1000;
parameter CNT_2S_MAX = 10'd1000;

//reg define
reg [6:0] cnt_2us;
reg [9:0] cnt_2ms;
reg [9:0] cnt_2s;
reg inc_dec_flag; //亮度递增/递减 0:递增 1:递减
reg [9:0] freq_step ; //呼吸灯频率间隔步长
reg led_t ;

//*****************************************************
//** main code
//*****************************************************
assign led = led_t & sw_ctrl;

//设置频率间隔,频率步长值在1-10之间
always @(posedge sys_clk) begin
if(!sys_rst_n)
freq_step <= START_FREQ_STEP;
else if(set_en) begin
if(set_freq_step == 0)
freq_step <= 10'd1;
else if(set_freq_step >= 10'd10)
freq_step <= 10'd10;
else
freq_step <= set_freq_step;
end
end

//cnt_2us:计数2us
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_2us <= 7'b0;
else if(cnt_2us == (CNT_2US_MAX - 7'b1 ))
cnt_2us <= 7'b0;
else
cnt_2us <= cnt_2us + 7'b1;
end

//cnt_2ms:计数2ms
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_2ms <= 10'b0;
else if(cnt_2ms == (CNT_2MS_MAX - 10'b1) && cnt_2us == (CNT_2US_MAX - 7'b1))
cnt_2ms <= 10'b0;
else if(cnt_2us == CNT_2US_MAX - 7'b1)
cnt_2ms <= cnt_2ms + 10'b1;
else
cnt_2ms <= cnt_2ms;
end

//cnt_2s:计数2s
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt_2s <= 10'b0;
else if(cnt_2s >= (CNT_2S_MAX - 10'b1) && cnt_2ms == (CNT_2MS_MAX - 10'b1) && cnt_2us == (CNT_2US_MAX - 7'b1))
cnt_2s <= 10'b0;
else if(cnt_2ms == (CNT_2MS_MAX - 10'b1) && cnt_2us == (CNT_2US_MAX - 7'b1))
cnt_2s <= cnt_2s + freq_step;
else
cnt_2s <= cnt_2s;
end

//inc_dec_flag为低电平,led灯由暗变亮,inc_dec_flag为高电平,led灯由亮变暗
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
inc_dec_flag <= 1'b0;
else if(cnt_2s >= (CNT_2S_MAX - 10'b1) && cnt_2ms ==( CNT_2MS_MAX - 10'b1) && cnt_2us == (CNT_2US_MAX - 7'b1))
inc_dec_flag <= ~inc_dec_flag;
else
inc_dec_flag <= inc_dec_flag;
end

//led:输出信号连接到外部的led灯
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_t <= 1'b0;
else if((inc_dec_flag == 1'b1 && cnt_2ms >= cnt_2s) || (inc_dec_flag == 1'b0 && cnt_2ms <= cnt_2s))
led_t <= 1'b1;
else
led_t<= 1'b0;
end

endmodule

编写完成相应的Verilog语言的文件之后,就可以对自定义IP核进行封装了,在封装过程中的一些操作,请直接参考视频吧:https://www.bilibili.com/video/BV1Et421H75m?t=12.2&p=42

注意,一定要看,里面还是有一些重要点的,比如makefile文件的修改。

8.5 软件程序

前面的小节封装好了自定义的IP核,下面就来开始学习本次实验的一个硬件搭建。其实本次实验的一个ZYNQ的配置跟前面所学习的AXI-GPIO实验的配置是一模一样的,所以可以打开那个AXI-GPIO实验的ZYNQ工程把它另存为本实验的工程,主要:只保留ZYNQ硬核的配置,其他的都可以先删掉。

8.5.1 创建Vivado工程

可以参考正点原子本节的视频开头部分,从零重新搭建一个适合本工程的ZYNQ工程。下面来添加呼吸灯的IP核(注意,如何没有,则大概率是没有把自定义IP的目录添加进来,所以需要先查看有没有把自定义IP核目录添加进来)。如果你想修改的自定义IP核的部分配置参数的话,可以双击打开自定义IP核进行修改,这里就不做修改。

OK下面让Vivado软件帮我们自动连接就可以了。自动连接完成后,下面只需要单击这个led的端口,把它引出到端口:

CTRL+S保存一下,然后验证一下这个设计有没有问题。没有问题即可生成顶层文件,然后进行编译。

另外就是本次工程还需要来添加PL端引脚的约束。我们来创建一个约束文件,就随便起名叫做pin,之前把LED的端口重命名为了pl_led,编写管脚约束代码。完成后,就可以来生成比特流文件。

全部完成后,把工程导出到硬件(包含比特流)。

8.5.2 创建Vitis工程

下面打开这个Vitis软件,选择当前工程的路径,快速创建裸机工程。完成后,创建main.c的文件,接下来就可以来编写代码了。怎么编写呢?可以参考自定义IP核生成的驱动文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "stdio.h"
#include "xparameters.h"
#include "xil_printf.h"
#include "breath_led_ip.h"
#include "xil_io.h"
#include "sleep.h"


#define LED_IP_BASEADDR XPAR_BREATH_LED_IP_0_S0_AXI_BASEADDR //LED IP基地址
#define LED_IP_REG0 BREATH_LED_IP_S0_AXI_SLV_REG0_OFFSET //LED IP寄存器地址0
#define LED_IP_REG1 BREATH_LED_IP_S0_AXI_SLV_REG1_OFFSET //LED IP寄存器地址1


int main()
{
int freq_flag; // 定义频率状态,用于循环改变呼吸灯的呼吸频率
int led_state; // 定义LED灯的状态

xil_printf("LED User IP Test!\r\n");

while(1)
{
// 根据freq_flag的标志位,切换呼吸灯的频率
if(freq_flag == 0)
{
BREATH_LED_IP_mWriteReg(LED_IP_BASEADDR, LED_IP_REG1, 0x80000001);
freq_flag = 1;
}
else
{
BREATH_LED_IP_mWriteReg(LED_IP_BASEADDR, LED_IP_REG1, 0x80000004);
freq_flag = 0;
}
// 获取LED当前开关状态 1:打开 0:关闭
led_state = BREATH_LED_IP_mReadReg(LED_IP_BASEADDR, LED_IP_REG0);
// 如果开关关闭,打开呼吸灯
if(led_state == 0)
{
BREATH_LED_IP_mWriteReg (LED_IP_BASEADDR, LED_IP_REG0, 1);
xil_printf("Breath LED ON\r\n");
}

sleep(8);
// 获取LED当前开关状态 1:打开 0:关闭
led_state = BREATH_LED_IP_mReadReg(LED_IP_BASEADDR, LED_IP_REG0);
//如果开关打开,关闭呼吸灯
if(led_state == 1)
{
BREATH_LED_IP_mWriteReg (LED_IP_BASEADDR, LED_IP_REG0, 0);
xil_printf("Breath LED OFF\r\n");
}
sleep(1);
}

8.6 修改自定义IP核和小结

参考链接:如何修改自定义的IP核和课程小结 - 正点原子 - Bilibili

9 UART外设

9.1 UART控制器介绍

9.1.1 UART控制器概念

UART控制器是一个全双工异步收发控制器,用于实现设备之间的串行通信,ZYNQ/MPSoC内部包含两个UART控制器——UART0和UART1。

9.1.2 UART控制器特性

  • 波特率可调
    • 支持可编程的波特率发生器,允许用户根据需要设置通信速率
  • 数据位/校验位/停止位可配置
    • 数据位可配置为6bit、7bit和8bit
    • 校验位可配置为奇(Odd)、偶(Even)、空格(Space)、标记(Mark)和无校验
    • 停止位可配置为1bit、1.5bit和2bit
  • 错误检测
    • 支持奇偶校验错误、帧错误和溢出错误检测
  • 环回模式
    • 支持正常模式、自动回音模式、本地环回模式和远程环回模式
  • 中断支持
    • 支持多种状态触发中断,如接收FIFO达到阈值、接收超时等

9.1.3 UART系统视图

UART系统视图主要展示它的端口以及主要功能模块。后面还会有一个系统框图,更详细地展开它内部的功能。

首先,要支持中断功能,那么肯定会有中断请求id,也即图中左上角IRQ ID(注意,再次强调中断请求ID是用来区分中断来自于哪个外设)。因为ZYNQ有2个UART,所以针对这个UART的控制器是有两个中断ID的5982

⚠️ 不同外设的中断ID也不用死记硬背,因为在编写代码的时候,这个软件里面都帮我们把不同外设的中断ID给使用个宏定义的方式给定义好了,直接把定义好的宏定义参数拿来用就行了。

然后,还需要提供参考的复位UART REF_RST和时钟REF_CLK

接着,一个比较关键的接口就是图中的APB从机接口,这个APB是一个高级外设接口,是AMBA接口总线的一部分(像前面学习的AXI接口呢,也是属于AMBA接口协议的一部分)。APB主要是用在一些低速的通信上面,比如对一些寄存器进行配置等。

中间左侧方框是UART控制器内部的一个视图,它包含UART接口的控制器UART Interface Controller和控制状态寄存器Control And Status Registers,对UART进行配置或者收发数据都是通过操作寄存器来实现的。

中间右侧方框有一个MIO-EMIO的路由MIO-EMIO Routing,也就是说你可以把UART的控制器的引脚连接到MIO引脚,也可以通过EMIO来连接到PL端的引脚。

🚦注意,从上图可以发现,它在连接到MIO或者是EMIO的时候会有一些差异:

  • 当连接到MIO引脚的时候呢可以发现它只有Tx和Rx两个引脚
  • 当通过EMIO连接到PL端引脚的时候,除了Tx和Rx之外,它还有CTSN、DCDN、DSRN等
    • CTSN,DCDN,DSRN,RIN,DTRN,RTSN这些引脚有一个专门的名词叫——modem control——一个调制解调器的控制,用于对整个串口的硬件数据流进行控制。它本质上是用来确保整个数据传输的可靠性和稳定性的,由于它会占用非常多的引脚,所以说这几个引脚一般用的也不多。只有在对整个通信要求稳定性更高的场景,比方说一些医疗设备或者是工业自动化等场景中使用。

9.1.4 UART系统框图

下图是UART的系统框图:

9.1.4.1 TxFIFO简介

详细看一下发送端的FIFO,因为这个FIFO比较关键。TxFIFO中存储从APB接口写入的数据,直到它被Transmitter读出并加载至移位寄存器中;用户通过写TxFIFO寄存器将数据写入TxFIFO中:

它是个FIFO,也即是一个先入先出的缓存器,所以说它可以产生一些标志位,这些标志位可以让它产生中断,如上图所示:

  • 满信号Full Interrupt[TFUL——当FIFO被写满时会置此标志位,并且可以产生中断。
  • 触发阈值Full Interrupt[TFUL]——比方说把FIFO阈值设置成5,那么FIFO中存储的数据达到5个数据之后,就会置此标志位,并且可以产生中断。
  • 空信号Empty Interrupt[TEMPTY]——当FIFO中是空的时会置此标志位,并且可以产生中断。
  • 将满信号Nearly Full Interrupt[TNFUL]——当FIFO存储的数据快要填满整个FIFO时会置此标志位,并且可以产生中断。
  • 溢出信号Overflow Interrupt[TOVR]——当FIFO写满之后,还要继续写数据,那么它会产生溢出中断。

9.1.4.2 RxFIFO简介

这个接收端的FIFO其实跟TxFIFO比较类似,就是一个反的过程:RxFIFO中存储从移位寄存器接收的数据,直到它被APB接口读出,用户通过读RxFIFO寄存器将数据从RxFIFO中读出;

9.1.4.3 UART的四种模式

下面来看一下UART的四种模式:

第一种是正常模式,就是一个正常的串口数据收发。一般正常操作就使用这种模式。

第二种是Automatic Echo Mode,给它翻译成自动回音模式,从示意图可以发现,对于接收端来说是正常的,但是发送端是断开的,所以说发送端实际上是没有用的,它所接收的数据一方面连写到RxFIFO中,另外一方面它又把它通过发送端口发出去了。可用于排查串口接收问题。

第三种是Local Loopback Mode,给它翻译成本地环回的模式,从示意图可以发现,它的发送器直接连接到接收器,并没有跟外部的引脚连接。那这种模式应用在什么场景呢?比方说大家的板卡没有USB串口,那么你就没有硬件来去调试这个接口对吧,但又想确认这个程序有没有问题,那只能是进行一个本地的还回,相当于发什么又还回过来了,从而来验证程序是否正确。

第四种是Remote Loopback Mode,给它翻译远程环回模式,它的接收端直接连接到发送端,并没有跟本地进行一个连接,全是是断开的。

9.1.5 领航者UART的PCB原理图

https://www.bilibili.com/video/BV1Et421H75m?t=1373.2&p=46

9.2 UART实验

9.2.1 UART实验任务

本节的实验任务是使用UART控制器,完成串口中断数据环回的功能。

9.2.2 系统框图

这就是一个最简单的系统框图。

9.2.3 Vivado工程创建

根据前面学习的内容,创建一个最简单Vivado工程即可:

打开配置:

设置波特率:

其他也不用改啥,然后重新生成比特流文件,然后导出硬件即可。

9.2.4 Vitis软件编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include "xparameters.h"	// 器件参数信息
#include "xuartps.h" // 包含PS UART的函数声明
#include "xil_printf.h" // 包含print()函数
#include "xscugic.h" // 包含中断的函数声明
#include "stdio.h" // 包含printf函数的声明

// ZYNQ有2个UART,可使用UART_DEVICE_ID指示使用的是哪个UART控制器
#define UART_DEVICE_ID XPAR_PS7_UART_0_DEVICE_ID //串口设备ID
#define UART_INT_IRQ_ID XPAR_XUARTPS_0_INTR //串口中断ID

#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID //中断控制器ID


XScuGic Intc; //中断控制器驱动程序实例
XUartPs Uart_Ps; //串口驱动程序实例

// UART初始化函数
int uart_init(XUartPs* uart_ps)
{
int status;
XUartPs_Config *uart_cfg;

// 根据UART的器件ID查找配置信息
uart_cfg = XUartPs_LookupConfig(UART_DEVICE_ID);
if (NULL == uart_cfg)
return XST_FAILURE;
// 对UART的驱动进行初始化
status = XUartPs_CfgInitialize(uart_ps, uart_cfg, uart_cfg->BaseAddress);
if (status != XST_SUCCESS)
return XST_FAILURE;

// UART设备自检
status = XUartPs_SelfTest(uart_ps);
if (status != XST_SUCCESS)
return XST_FAILURE;

// 设置工作模式:正常模式
XUartPs_SetOperMode(uart_ps, XUARTPS_OPER_MODE_NORMAL);
// 设置波特率:115200
XUartPs_SetBaudRate(uart_ps, 115200);
// 设置RxFIFO的中断触发等级
XUartPs_SetFifoThreshold(uart_ps, 1);

return XST_SUCCESS;
}

// UART中断处理函数
void uart_intr_handler(void *call_back_ref)
{
XUartPs *uart_instance_ptr = (XUartPs *)call_back_ref;
u32 rec_data = 0 ;
u32 isr_status ; //中断状态标志

// 读取中断ID寄存器,判断触发的是哪种中断
isr_status = XUartPs_ReadReg(uart_instance_ptr->Config.BaseAddress,XUARTPS_IMR_OFFSET);
isr_status &= XUartPs_ReadReg(uart_instance_ptr->Config.BaseAddress,XUARTPS_ISR_OFFSET);

// 判断中断标志位RxFIFO是否触发
if (isr_status & (u32)XUARTPS_IXR_RXOVR)
{
rec_data = XUartPs_RecvByte(XPAR_PS7_UART_0_BASEADDR);
// 清除中断标志
XUartPs_WriteReg(uart_instance_ptr->Config.BaseAddress,XUARTPS_ISR_OFFSET, XUARTPS_IXR_RXOVR);
}
// 发送数据
XUartPs_SendByte(XPAR_PS7_UART_0_BASEADDR, rec_data);
}

// 串口中断初始化
int uart_intr_init(XScuGic *intc, XUartPs *uart_ps)
{
int status;
// 初始化中断控制器:根据中断控制器的器件ID查找配置信息
XScuGic_Config *intc_cfg;
intc_cfg = XScuGic_LookupConfig(INTC_DEVICE_ID);
if (NULL == intc_cfg)
return XST_FAILURE;
// 根据查找到的配置信息初始化中断控制器
status = XScuGic_CfgInitialize(intc, intc_cfg, intc_cfg->CpuBaseAddress);
if (status != XST_SUCCESS)
return XST_FAILURE;

// 设置并打开中断异常处理功能
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
(void *)intc);
Xil_ExceptionEnable();

// 为中断设置中断处理函数
XScuGic_Connect(intc, UART_INT_IRQ_ID,
(Xil_ExceptionHandler)uart_intr_handler, (void *)uart_ps);
// 设置UART的中断触发方式:RxFIFO触发中断
XUartPs_SetInterruptMask(uart_ps, XUARTPS_IXR_RXOVR);
// 使能GIC中的串口中断
XScuGic_Enable(intc, UART_INT_IRQ_ID);
return XST_SUCCESS;
}


int main(void)
{
int status;
// 串口初始化
status = uart_init(&Uart_Ps);
if (status == XST_FAILURE)
{
xil_printf("Uart Initial Failed\r\n");
return XST_FAILURE;
}

// 串口中断初始化
uart_intr_init(&Intc, &Uart_Ps);

while (1);
return status;
}

9.3 小结

课程总结部分和答疑部分直接看原视频吧,没有特别重要的知识点:https://www.bilibili.com/video/BV1Et421H75m?t=3.7&p=50

10 定时器外设