安路HDMI软硬测试
🚃

安路HDMI软硬测试

日期
Status
Done
Assign
Description
当程序被烧录到微控制器后,微控制器在执行程序时,会通过其内部的总线结构(如AHBLite)来访问程序代码和数据。因此,虽然HEX文件中的数据最终会通过AHBLite总线被CPU访问和执行,但HEX文件本身并不直接与AHBLite总线交互。总线的使用是由硬件设备的内部逻辑和微控制器的架构决定的。
notion image
我们现在终于可以回过头来再重新审视这张图片。软硬协同的编程方式要求我们同步进行硬件以及软件的程序编写工作。那么,什么是软件操作,什么是硬件操作呢?
  • 软件操作,即通过 keil 来完成 C语言程序编写,通过高级语言来完成对硬件功能地调配。
  • 硬件操作,即通过 verilog 等硬件语言完成底层逻辑地搭建,实现基本的接口连接,完成各个设备间的信息传输。
传统而言,我们仅通过 verilog 就可以完成很多特定的需求,这也便是 FPGA 的基本工作原理,例如用 verilog 实现硬件流水灯等。当我们搭建了底层硬件逻辑,再用 C语言来完成顶层操作时,这也变成了嵌入式的开发,例如,我们同样可以用 C语言来实现流水灯。但没有底层硬件是无法单独用 C语言等高级语言完成逻辑操控的。
我们现在尝试用一个小的项目实现对 cortex-M0 内核的软硬协同操作,功能很简单,我只需要一个控制开关,在 keil 里实现打开或关闭 hdmi 显示接口。
我们基于蜂鸣器实验来进行程序魔改,加入对 hdmi 显示的操控。hdmi 控制器在硬件里实现,顶层不需要再编写响应控制程序,我只预留一个接口 display_on 用来控制开启或关闭。
module AHBlite_HDMI( input wire HCLK, input wire HRESETn, input wire HSEL, input wire [31:0] HADDR, input wire [2:0] HBURST, input wire HMASTLOCK, input wire [1:0] HTRANS, input wire [2:0] HSIZE, input wire [3:0] HPROT, input wire HWRITE, input wire [31:0] HWDATA, input wire HREADY, output wire HREADYOUT, output wire[31:0] HRDATA, output wire HRESP, output wire display_on //HDMI显示控制接口 ); assign HRESP=1'b0; assign HREADYOUT=1'b1; wire display_en; assign display_en=HSEL & HTRANS[1] & HWRITE & HREADY; reg dis_en_reg; always@(posedge HCLK or negedge HRESETn) begin if(~HRESETn) dis_en_reg<=1'b0; else if(display_en) dis_en_reg<=1'b1; else dis_en_reg<=dis_en_reg; end assign display_on = dis_en_reg ? 1'b1 : 1'b0; endmodule
上面的代码实现了挂载在 ahblite 总线上,并实现了对 HDMI 控制器的顶层控制。
assign display_en=HSEL & HTRANS[1] & HWRITE & HREADY;实现了内部控制线,当 MUX 信号选中先前分配的 HDMI 控制地址时,display_en 信号被拉高,之后创建一个临时寄存器 dis_en_reg 用来存储这种被选中的状态,也就是说,一旦使用到这个地址时,dis_en_reg 即被拉高并锁存,表示存下了当前使用到了这个地址的状态,那我们通过这种状态锁存在来进一步控制 HDMI 的开关,使用assign display_on = dis_en_reg ? 1'b1 : 1'b0;语句,这种被选中的状态会让 display_on 信号同样变为高电平,那接下来就就可以实现对 HDMI 的开启操作。
🤓那我们是是怎么分配地址的呢?
这一步需要在AHBlite_Decoder.v 文件中进行操作。AHBlite_Decoder.v 模块被专门用来进行译码操作:
module AHBlite_Decoder #( /*RAMCODE enable parameter*/ parameter Port0_en = 1, /************************/ /*RAMDATA enable parameter*/ parameter Port1_en = 1, /************************/ /*keyboard enable parameter*/ parameter Port2_en = 1, /************************/ /*buzzermusic enable parameter*/ parameter Port3_en= 1, /************************/ /*hdmi enable parameter*/ parameter Port4_en= 1 /************************/ )( input [31:0] HADDR, /*RAMCODE OUTPUT SELECTION SIGNAL*/ output wire P0_HSEL, /*RAMDATA OUTPUT SELECTION SIGNAL*/ output wire P1_HSEL, /*keyboard OUTPUT SELECTION SIGNAL*/ output wire P2_HSEL, /*buzzermusic OUTPUT SELECTION SIGNAL*/ output wire P3_HSEL, /*hdmi OUTPUT SELECTION SIGNAL*/ output wire P4_HSEL ); //RAMCODE----------------------------------- //0x00000000-0x0000ffff /*Insert RAMCODE decoder code there*/ assign P0_HSEL = (HADDR[31:16] == 16'h0000) ? Port0_en : 1'b0; /***********************************/ //RAMDATA----------------------------- //0x20000000-0x2000ffff /*Insert RAMDATA decoder code there*/ assign P1_HSEL = (HADDR[31:16] == 16'h2000) ? Port1_en : 1'b0; /***********************************/ //------------------------------ //0x40000000 key_data/key_clear assign P2_HSEL =(HADDR[31:4] == 28'h4000000) ? Port2_en : 1'b0; /***********************************/ //0x40000010 buzzermusic select/en assign P3_HSEL =(HADDR[31:4] == 28'h4000001) ? Port3_en : 1'b0; /***********************************/ //0x40000020 hdmi select/en assign P4_HSEL =(HADDR[31:4] == 28'h4000002) ? Port4_en : 1'b0; endmodule
我们将目光放在文件最后:assign P4_HSEL =(HADDR[31:4] == 28'h4000002) ? Port4_en : 1'b0;,这一句话的意思就是:当我们的选中的地址的前 28 位对应 28'h4000002 时,即表示选中了 P4_HSEL 信号。由于我们先前已经做好了底层的连接,P4_HSEL 信号正是连接的 AHBlite_HDMI 模块中的 HSEL 信号,当顶层软件生成的程序用到28'h4000002 开头的地址时,P4_HSEL 信号响应这种操作而被拉高,AHBlite_HDMI 模块被选中而开启,接下来就完成了上面所说的操作,即可以为 display_on 信号赋高电位。
😣刚开始时你可能不太理解,这里的地址我是任意分配的吗?为什么可以这么分配?为什么分配了就可以实现对 AHBlite_HDMI 模块的操作?
想要深入理解这些需要深入到计算机操作底层逻辑中,我们这里不做过多的解释。我想表达的是,在内核地址大小合理的情况下,这个地址分配是任意的,只要你乐意就好。比如,我在注释中就写到了://0x40000020 hdmi select/en,这就表示我希望把这个地址分配给 HDMI 的操控接口,至于紧接着的那句 assign 语句,你也可以严格匹配,比如改成assign P4_HSEL =(HADDR[31:0] == 28'h40000020) ? Port4_en : 1'b0;,这样你就严格限定了这一个地址才对应P4_HSEL 信号。而程序中的这种写法增加了地址灵活性,你可以在最后一位用别的数字代替,而不用非得也是 0。
怎么使用这种地址呢?那就要到顶层软件中进行操作,也就是 keil 中。
#define HDMI (*(volatile unsigned*) 0x40000020)
在 keil 的 code_def.h 文件中,我们定义了一些地址,并给它们赋上了一些名称便于操作。上面这个语句的意思便是,代码中每次使用 HDMI,预处理器都会将其替换为 (*(volatile unsigned*) 0x40000020),也就是对这个地址的操作。接下来,我们进入 main 函数:
#include <stdint.h> #include "code_def.h" #include <string.h> #include <stdio.h> extern uint32_t key_flag; int __main() { NVIC_CTRL_ADDR=1; HDMI = 1; //使用0x40000020地址 while(1) { while(!key_flag); uint32_t din; din=Keyboard_keydata_clear; int i=0; int ans=0; for(i=0;i<16;i++) { if((din>>i)&1) { ans=i; Music_data=16+ans; break; } } key_flag=0; Keyboard_keydata_clear=1; } }
在第 12 行我们清晰的发现了对 HDMI 的调用,这就是告诉程序,到这一步时我们要用到这个地址了!程序记住了这句话的意思,当执行到这一步时就选中 0x40000020 地址位。
👀好了,我们现在底层硬件搭好了,软件层面的开启也使用到了,那怎么把二者联系来呢?
这里我们就要再来到硬件代码里的 Block_RAM.v 模块:
module Block_RAM #( parameter ADDR_WIDTH = 12 ) ( input clka, input [ADDR_WIDTH-1:0] addra, input [ADDR_WIDTH-1:0] addrb, input [31:0] dina, input [3:0] wea, output reg [31:0] doutb ); (* ram_style="block" *)reg [31:0] mem [(2**ADDR_WIDTH-1):0]; initial begin $readmemh("D:/Desktop/hdmi_display_copy20240504/keil/code.hex",mem); end always@(posedge clka) begin if(wea[0]) mem[addra][7:0] <= dina[7:0]; end always@(posedge clka) begin if(wea[1]) mem[addra][15:8] <= dina[15:8]; end always@(posedge clka) begin if(wea[2]) mem[addra][23:16] <= dina[23:16]; end always@(posedge clka) begin if(wea[3]) mem[addra][31:24] <= dina[31:24]; end always@(posedge clka) begin doutb <= mem[addrb]; end endmodule
第 15 行有一个关键语句$readmemh("D:/Desktop/hdmi_display_copy20240504/keil/code.hex",mem);。这个语句中的$readmemh 命令意思是:用于将十六进制数据从文件加载到Verilog的内存数组中。当我们用 keil 软件编译过 C语言程序后,会产生一个 .hex 文件,这是一个十六进制文件,包含我们想用软件对底层硬件的操作逻辑,这是个十六进制的机器码文件,是不可读的。而我们在程序中这么写,就是希望把编译过后的机器码文件放进我们预先定义的 mem 寄存器,用来存放我们的指令数据。
现在,当你再去 TD 软件中编译整个硬件工程,你在 keil 里编写好的 C语言程序转化后的机器码也就被吃进了硬件逻辑中,保存在了硬件内部 RAM 里。正如我们开篇提到的那样,“hex 文件中的数据最终会通过AHBLite总线被CPU访问和执行”当硬件执行到HDMI = 1;语句编译后产生的机器码时,总线就会执行 0x40000020 地址操作,由于我们在AHBlite_HDMI.v 中设定了,只要 CPU 访问了这个地址,display_on 就会变成高位,那么进一步的,就会打开 hdmi 显示。
如此一来,通过软件产生访问指令进而操作硬件工作的流程也就实现了!
如果你想验证一下究竟是不是软件操控的 hdmi 显示,你可以在 C语言中将 HDMI = 1;语句注释掉 (注释掉的意思就是程序没用到这个0x40000020 地址,也就不会触发接下来将 display_on 信号拉高的操作),重新编译后再把 .hex 文件导入到硬件中,看屏幕是不是灭的,如果不亮,那就证明了软件对这个硬件地址,也就是 hdmi 显示的直接控制。
notion image
HDMI设计主要分为硬件和软件两部分。硬件部分主要包括基于Verilog的底层逻辑搭建,如AHBlite总线和HDMI控制器的实现。软件部分则是通过C语言编写程序进行硬件操作。通过软件产生访问指令,CPU在得到这些指令后通过AHBlite总线访问和执行它们,从而实现软硬协同操作。例如,可以控制HDMI的开启或关闭。
项目拓展