PythonとJinjaがFPGA開発者の生活をより簡単にする方法

みなさんこんにちは!



使用されているプログラミング言語が、私たちがやりたいことに制限を課し、開発に不便をもたらすことがあります。 開発者はこれで何をしますか? または自分自身を辞任するか、どういうわけか状況から脱出しようとします。



1つのオプションは、コードの自動生成を使用することです。



この記事では、次のことを説明します。





興味があれば、キャットへようこそ!





モジュールのパラメーター化



Verilog-2001言語で開発しており、単純なマルチプレクサを作成する必要があると想定しています。



module simple_mux #( parameter D_WIDTH = 8 ) ( input [D_WIDTH-1:0] data_0_i, input [D_WIDTH-1:0] data_1_i, input sel_i, output [D_WIDTH-1:0] data_o ); assign data_o = ( sel_i ) ? ( data_1_i ): ( data_0_i ); endmodule
      
      







ここで複雑なことはありません。



ただし、24/48ポート用のある種のスイッチを開発している場合、ある時点でパケットスイッチングが行われるモジュールが必要になります(これにはマルチポートマルチプレクサが必要です)。



手動で行うには:

 input [D_WIDTH-1:0] data_0_i, input [D_WIDTH-1:0] data_1_i, ... input [D_WIDTH-1:0] data_47_i,
      
      







あまり正しくありません。 実際には、データだけでなく、各データストリーム(読み取り、ポート)ごとに1つではなく複数の信号が存在する専用のインターフェイスが必要です。



魂はこのようなことを書くように求めます:



 module simple_mux #( parameter D_WIDTH = 8, parameter PORT_CNT = 48, // internal param parameter SEL_WIDTH = $clog2( PORT_CNT ) ) ( input [D_WIDTH-1:0] data_i [PORT_CNT-1:0], input [SEL_WIDTH-1:0] sel_i, output [D_WIDTH-1:0] data_o ); assign data_o = data_i[ sel_i ]; endmodule
      
      







ただし、モジュールポートでのアレイの使用は、IEEE 1364-2001標準(Verilog-2001について説明されます) では許可されいません



そのため、たとえば、 Quartusは次のエラーを生成します。

Error (10773): Verilog HDL error: declaring module ports or function arguments with unpacked array types requires SystemVerilog extensions









どうする?



非表示のテキスト
SystemVerilogを使用します。

非表示のテキスト
:)







考えられる回避策の1つがStackOverflowで検討されています。 アイデアは機能していますが、記事はそれについてではありません:)



もちろん、ハンドルをあきらめて必要なワイヤを作ることができますが、マシンがこれを行う方が良いです:

Jinja2テンプレートエンジンのTemplateクラスを使用します



  t = Template(u""" module {{name}} #( parameter D_WIDTH = 8 ) ( {%- for p in ports %} input [D_WIDTH-1:0] data_{{p}}_i, {%- endfor %} input [{{sel_width-1}}:0] sel_i, output [D_WIDTH-1:0] data_o ); always @(*) begin case( sel_i ) {% for p in ports %} {{sel_width}}'d{{p}}: begin data_o = data_{{p}}_i; end {% endfor %} endcase end endmodule """) print t.render( n = 4, sel_width = 2, name = "simple_mux_4habr", ports = range( 4 ) )
      
      







モジュールテンプレートを作成し、複製に必要なもの説明するために使用 、必要なパラメーター(ポートの数、モジュール名など)を渡してrenderをプルしました。 {{}}を使用するこれらのパラメーターはテンプレートで使用でき、目的の場所に変数を挿入できます。



出力は素晴らしいモジュールでした:

非表示のテキスト
 module simple_mux_4habr #( parameter D_WIDTH = 8 ) ( input [D_WIDTH-1:0] data_0_i, input [D_WIDTH-1:0] data_1_i, input [D_WIDTH-1:0] data_2_i, input [D_WIDTH-1:0] data_3_i, input [1:0] sel_i, output [D_WIDTH-1:0] data_o ); always @(*) begin case( sel_i ) 2'd0: begin data_o = data_0_i; end 2'd1: begin data_o = data_1_i; end 2'd2: begin data_o = data_2_i; end 2'd3: begin data_o = data_3_i; end endcase end endmodule
      
      









美しさは、変数として渡すことができるのは数字や文字列だけでなく、Pythonタイプ(シート、辞書、クラスのオブジェクト)であり、Pythonコードと同じ方法でテンプレートにアクセスできることです。 たとえば、辞書要素の呼び出しは次のようになります。

 {{ foo['bar'] }}
      
      







もちろん、これは単純な例であり、おそらくperl / sed / awkなどを使用して実行できます。



Jinjaについて読んで、簡単な例で遊んでみたとき、もっと深刻なことに使用できるのではないかと思いました。 FPGAの開発で発生する1つの問題を思い出しましたが、これは十分に自動化されているようです。 このタスクをスムーズに進めるために、開発の構成について少し説明します。



IPコア



ASIC / FPGAの迅速な開発の基礎は、IPコアとして設計された既製のコードの使用であると考えられています。 詳細に入ることなく、IPコアはライブラリであると想定できます。



アイデアは、すべてのファームウェアがIPコアに分割され、それが自分で書き込まれるか、購入されて盗まれたり、 クラックされたり、標準インターフェイス(AXIやAvalonなど)を使用して接続されることです。 接続は、ハンドルまたはGUIアプリケーションの助けを借りて行うことができます。GUIアプリケーションでは、必要なカーネルをクリックしてマウスで接続できます。 たとえば、 Quartusに付属するQsysです。



このアプローチの利点は明らかです。





マイナス面の1つは、標準インターフェイスを介して接続するためのオーバーヘッドがあるという事実です。これは、より多くのコードまたはより多くのリソース(セル)を占有する可能性があります。



各コアには、 CSRステータスレジスタのセットがあります。



ほとんどの場合、それらは単語(たとえば32ビット)に従ってグループ化され、内部ではフィールドによって分割されます。フィールドは異なる動作モードを持つことができます。





同じレジスタ内にはいくつかのフィールドがあり、それらの動作モードは異なる場合があります。



単純なI2Cエクスパンダーからトランシーバー、さらにはネットワークカードに至るまで、さまざまなチップのCSRもあります。



トップレベルのプログラマーはどのように見えますか?

アルテラのトリプルスピードイーサネット MACコントローラを検討してください。 ドキュメントを開いて 構成レジスタ空間 」の章に進むと、カーネルを制御し、その状態に関する情報(受信/送信パケットのカウンターなど)を受信できるすべてのレジスタのリストが表示されます。



レジスタが説明されている表の一部を示します。

画像



ところで、これらの行を読み取らせるパケットがこのIPコアを通過する可能性があります。



たとえば、このカーネルのレジスタ0x03および0x04は、MACアドレスの設定を担当します。 一部のカーネル(ザイリンクスまたはIntel製)では、他のレジスタを使用できます。



ドライバの MACアドレスの変更は次のとおりです



 static void tse_update_mac_addr(struct altera_tse_private *priv, u8 *addr) { u32 msb; u32 lsb; msb = (addr[3] << 24) | (addr[2] << 16) | (addr[1] << 8) | addr[0]; lsb = ((addr[5] << 8) | addr[4]) & 0xffff; /* Set primary MAC address */ csrwr32(msb, priv->mac_dev, tse_csroffs(mac_addr_0)); csrwr32(lsb, priv->mac_dev, tse_csroffs(mac_addr_1)); }
      
      







mac_addr_0mac_addr_1は0x03と0x04にすぎず、これらは非常にトリッキーです(私の主観的な意見では、ドライバーではこれが正常であると想定しています)が、隣接するヘッダーファイルで定義されています



IPコアの開発者は、すべてのCSRを説明するドキュメントを提供し、同様に、何を、どのように、どの順序で構成するかを説明します。 このドキュメントは高レベルのプログラマーに渡され、 tse_update_mac_addrに似た関数を作成し、すべて機能するようにします:)



マルチコアシステム



多くの場合、1つのコアによってもたらされるタスクは解決できません。システムにはそれらのいくつかがあります。

管理インターフェイスを1つのバスに接続して、各コアに独自のアドレススペースを割り当てることができます。



上位レベルがカーネルBレジスタを0x03に書き込む必要がある場合、アドレス0x0103でトランザクションを実行する必要があります。 (簡単にするために、アドレスはバイトではなく、単語に従っていると考えています。実際には、バイトアドレスで書き込む必要があることが判明する場合があります。その後、32ビットレジスタのリクエストは0x010Cのトランザクションになります)。



画像



マスター(CPU(ARM / x86)またはMCU、または他の一般的なIPコア)は、管理インターフェイスを介して、読み取りまたは書き込みトランザクションを実行します。 IPコア管理インターフェイスは、多くの場合、標準(AXIまたはAvalon)のいずれかに従って行われます。



複数のスレーブがある場合、インターコネクトモジュール(マルチプレクサまたはバスアービター)が表示されます。 そのタスクは、マスターからのリクエストを受け入れ、このリクエストの送信先を確認することです。これは、スレーブが応答している間などにバスを保持できるなどの方法です。 したがって、このモジュールの前は、リクエストアドレスは0x0103でしたが、その後は-0x0003でした。 IPコアは、どのアドレススペースが割り当てられているかを認識していません(認識すべきではない)。



特定のIPコアを分解します(問題を示します)



IPコアの内部には、これらすべてのレジスタを含むモジュールがあり、それらをIPコアの内部にあるが外部からは見えないモジュールを制御するための信号セットに変換する必要があります。



指で話さないようにするために、イーサネットパケットジェネレーターの非常に単純な抽象的なIPコアを検討します。これは、たとえば測定 機器で使用できます。



このカーネルにこれらのレジスタを持たせてください:

 0x0: [7:0] - IP_CORE_VERSION [RO] -  IP- [15:8] - Reserved [16] - GEN_EN [RW] -   [17] - GEN_ERROR [ROLH] -     [30:18] - Reserved [31] - GEN_RESET [RWSC] -   0x1: [31:0] - IP_DST [RW] - IP-   0x2: [15:0] - FRM_SIZE [RW] -   [31:16] - Reserved 0x3: [31:0] - FRM_CNT [RO] -   
      
      







IPコア自体は次のようになります。

画像



csr_mapモジュールは、標準インターフェイスを、主要なカーネル機能を実行するtraffic_generatorモジュールの制御信号のセットに「変換」する役割を果たします。 もちろん、IPコアが2つのモジュールのみで構成されることはほとんどありません。ほとんどの場合、制御信号はIPコア内の複数のモジュールに配信されます。



私が何を得ているかを推測してください:

これらのレジスタの説明からこのcsr_mapを自動的生成することは可能ですか?



実際には、約100のレジスタが存在する可能性があり、これが自動化されている場合は、次のようになります。





問題を解決する



レジスタとビット(フィールド)によって情報を格納するための2つのプリミティブクラスを作成します。

 class Reg( ): def __init__( self, num, name ): self.num = '{:X}'.format( num ) self.name = name self.name_lowcase = self.name.lower() self.bits = [] def add_bits( self, reg_bits ): self.bits.append( reg_bits ) class RegBits( ): def __init__( self, bit_msb, bit_lsb, name, mode = "RW", init_value = 0 ): self.bit_msb = bit_msb self.bit_lsb = bit_lsb self.width = bit_msb - bit_lsb + 1 self.name = name self.name_lowcase = self.name.lower() self.mode = mode self.init_value = '{:X}'.format( init_value ) # bit modes: # RO - read only # RO_CONST - read only, constant value # RO_LH - read only, latch high # RO_LL - read only, latch low # RW - read and write # RW_SC - read and write, self clear assert self.mode in ["RO", "RO_CONST", "RO_LH", "RO_LL", "RW", "RW_SC" ], "Unknown bit mode" if self.mode in ["RO_LH", "RO_LL", "RW_SC"]: assert self.width == 1, "Wrong width for this bit mod" self.port_signal_input = self.mode in ["RO", "RO_LH", "RO_LL"] self.port_signal_output = self.mode in ["RW", "RW_SC"] self.need_port_signal = self.port_signal_input or self.port_signal_output
      
      







これらのクラスを使用して、CSR記述を作成します。

  MODULE_NAME = "trafgen_map_4habr" r0 = Reg( 0x0, "MAIN") r0.add_bits( RegBits( 7, 0, "IP_CORE_VERSION", "RO_CONST", 0x7 ) ) r0.add_bits( RegBits( 16, 16, "GEN_EN" , "RW" ) ) r0.add_bits( RegBits( 17, 17, "GEN_ERROR", "RO_LH" ) ) r0.add_bits( RegBits( 31, 31, "GEN_RESET", "RW_SC" ) ) r1 = Reg( 0x1, "IP_DST" ) # let ip destination in reset will be 178.248.233.33 ( habrahabr.ru ) r1.add_bits( RegBits( 31, 0, "IP_DST", "RW", 0xB2F8E921 ) ) r2 = Reg( 0x2, "FRM_SIZE" ) r2.add_bits( RegBits( 15, 0, "FRM_SIZE", "RW", 64 ) ) r3 = Reg( 0x3, "FRM_CNT" ) r3.add_bits( RegBits( 31, 0, "FRM_CNT", "RO" ) ) reg_l = [r0, r1, r2, r3]
      
      







テンプレート自体は次のようになります。

非表示のテキスト
 csr_map_template = Template(u""" {%- macro reg_name( r ) -%} reg_{{r.num}}_{{r.name_lowcase}} {%- endmacro %} {%- macro reg_name_bits( r, b ) -%} reg_{{r.num}}_{{r.name_lowcase}}___{{b.name_lowcase}} {%- endmacro %} {%- macro bit_init_value( b ) -%} {{b.width}}'h{{b.init_value}} {%- endmacro %} {%- macro signal( width ) -%} [{{width-1}}:0] {%- endmacro %} {%- macro print_port_signal( dir, width, name, eol="," ) -%} {{ " %-12s %-10s %-10s" | format( dir, signal( width ), name+eol ) }} {%- endmacro %} {%- macro get_port_name( b ) -%} {%- if b.port_signal_input -%} {{b.name_lowcase}}_i {%- else -%} {{b.name_lowcase}}_o {%- endif -%} {%- endmacro -%} // Generated using CSR map generator // https://github.com/johan92/csr-map-generator module {{module_name}}( {%- for p in data %} // Register {{p.name}} signals {%- for b in p.bits %} {%- if b.port_signal_input %} {{print_port_signal( "input", b.width, get_port_name( b ) )}} {%- elif b.port_signal_output %} {{print_port_signal( "output", b.width, get_port_name( b ) )}} {%- endif %} {%- endfor %} {% endfor %} // CSR interface {{print_port_signal( "input", 1, "reg_clk_i" ) }} {{print_port_signal( "input", 1, "reg_rst_i" ) }} {{print_port_signal( "input", reg_d_w, "reg_wr_data_i" ) }} {{print_port_signal( "input", 1, "reg_wr_en_i" ) }} {{print_port_signal( "input", 1, "reg_rd_en_i" ) }} {{print_port_signal( "input", reg_a_w, "reg_addr_i" ) }} {{print_port_signal( "output", reg_d_w, "reg_rd_data_o", "" ) }} ); {%- for p in data %} // ****************************************** // Register {{p.name}} // ****************************************** logic [{{reg_d_w-1}}:0] {{reg_name( p )}}_read; {%- for b in p.bits %} {%- if b.mode != "RO" %} logic [{{b.width-1}}:0] {{reg_name_bits( p, b )}} = {{bit_init_value( b )}}; {%- endif %} {%- endfor %} {% for b in p.bits %} {%- if b.port_signal_output %} always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) {{reg_name_bits( p, b )}} <= {{bit_init_value( b )}}; else if( reg_wr_en_i && ( reg_addr_i == {{reg_a_w}}'h{{p.num}} ) ) {{reg_name_bits( p, b )}} <= reg_wr_data_i[{{b.bit_msb}}:{{b.bit_lsb}}]; {%-if b.mode == "RW_SC" %} else {{reg_name_bits( p, b )}} <= {{bit_init_value( b )}}; {% endif %} {%- endif %} {%- if b.mode == "RO_LH" or b.mode == "RO_LL" %} always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) {{reg_name_bits( p, b )}} <= {{bit_init_value( b )}}; else begin if( reg_rd_en_i && ( reg_addr_i == {{reg_a_w}}'h{{p.num}} ) ) {{reg_name_bits( p, b )}} <= {{bit_init_value( b )}}; {% if b.mode == "RO_LL" %} if( {{get_port_name( b )}} == 1'b0 ) {{reg_name_bits( p, b )}} <= 1'b0; {%- elif b.mode == "RO_LH" %} if( {{get_port_name( b )}} == 1'b1 ) {{reg_name_bits( p, b )}} <= 1'b1; {%- endif %} end {% endif %} {% endfor %} // assigning to output {%- for b in p.bits %} {%- if b.port_signal_output %} assign {{get_port_name( b )}} = {{reg_name_bits( p, b )}}; {%- endif %} {%- endfor %} {%- macro print_in_always_comb( r, b, _right_value ) -%} {%- if b == "" -%} {{ " %s%-7s = %s;" | format( reg_name( r ) + "_read", "", _right_value ) }} {%- else -%} {{ " %s%-7s = %s;" | format( reg_name( r ) + "_read", "["+b.bit_msb|string+":"+b.bit_lsb|string+"]" , _right_value ) }} {%- endif -%} {%- endmacro %} // assigning to read data always_comb begin {{print_in_always_comb( p, "", reg_d_w|string+"'h0" ) }} {%- for b in p.bits %} {%- if b.mode == "RO" %} {{print_in_always_comb( p, b, get_port_name( b ) )}} {%- else %} {{print_in_always_comb( p, b, reg_name_bits( p, b ) )}} {%- endif %} {%- endfor %} end {%- endfor %} // ****************************************** // Reading stuff // ****************************************** logic [{{reg_d_w-1}}:0] reg_rd_data = {{reg_d_w}}'h0; always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_rd_data <= {{reg_d_w}}'h0; else if( reg_rd_en_i ) begin case( reg_addr_i ) {% for p in data %} {{reg_a_w}}'h{{p.num}}: begin reg_rd_data <= {{reg_name( p )}}_read; end {% endfor %} default: begin reg_rd_data <= {{reg_d_w}}'h0; end endcase end assign reg_rd_data_o = reg_rd_data; endmodule """)
      
      









テンプレート生成を引き出します:

  res = csr_map_template.render( module_name = MODULE_NAME, reg_d_w = 32, reg_a_w = 8, data = reg_l )
      
      







そのようなモジュールがあります:

非表示のテキスト
 // Generated using CSR map generator // https://github.com/johan92/csr-map-generator module trafgen_map_4habr( // Register MAIN signals output [0:0] gen_en_o, input [0:0] gen_error_i, output [0:0] gen_reset_o, // Register IP_DST signals output [31:0] ip_dst_o, // Register FRM_SIZE signals output [15:0] frm_size_o, // Register FRM_CNT signals input [31:0] frm_cnt_i, // CSR interface input [0:0] reg_clk_i, input [0:0] reg_rst_i, input [31:0] reg_wr_data_i, input [0:0] reg_wr_en_i, input [0:0] reg_rd_en_i, input [7:0] reg_addr_i, output [31:0] reg_rd_data_o ); // ****************************************** // Register MAIN // ****************************************** logic [31:0] reg_0_main_read; logic [7:0] reg_0_main___ip_core_version = 8'h7; logic [0:0] reg_0_main___gen_en = 1'h0; logic [0:0] reg_0_main___gen_error = 1'h0; logic [0:0] reg_0_main___gen_reset = 1'h0; always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_0_main___gen_en <= 1'h0; else if( reg_wr_en_i && ( reg_addr_i == 8'h0 ) ) reg_0_main___gen_en <= reg_wr_data_i[16:16]; always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_0_main___gen_error <= 1'h0; else begin if( reg_rd_en_i && ( reg_addr_i == 8'h0 ) ) reg_0_main___gen_error <= 1'h0; if( gen_error_i == 1'b1 ) reg_0_main___gen_error <= 1'b1; end always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_0_main___gen_reset <= 1'h0; else if( reg_wr_en_i && ( reg_addr_i == 8'h0 ) ) reg_0_main___gen_reset <= reg_wr_data_i[31:31]; else reg_0_main___gen_reset <= 1'h0; // assigning to output assign gen_en_o = reg_0_main___gen_en; assign gen_reset_o = reg_0_main___gen_reset; // assigning to read data always_comb begin reg_0_main_read = 32'h0; reg_0_main_read[7:0] = reg_0_main___ip_core_version; reg_0_main_read[16:16] = reg_0_main___gen_en; reg_0_main_read[17:17] = reg_0_main___gen_error; reg_0_main_read[31:31] = reg_0_main___gen_reset; end // ****************************************** // Register IP_DST // ****************************************** logic [31:0] reg_1_ip_dst_read; logic [31:0] reg_1_ip_dst___ip_dst = 32'hB2F8E921; always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_1_ip_dst___ip_dst <= 32'hB2F8E921; else if( reg_wr_en_i && ( reg_addr_i == 8'h1 ) ) reg_1_ip_dst___ip_dst <= reg_wr_data_i[31:0]; // assigning to output assign ip_dst_o = reg_1_ip_dst___ip_dst; // assigning to read data always_comb begin reg_1_ip_dst_read = 32'h0; reg_1_ip_dst_read[31:0] = reg_1_ip_dst___ip_dst; end // ****************************************** // Register FRM_SIZE // ****************************************** logic [31:0] reg_2_frm_size_read; logic [15:0] reg_2_frm_size___frm_size = 16'h40; always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_2_frm_size___frm_size <= 16'h40; else if( reg_wr_en_i && ( reg_addr_i == 8'h2 ) ) reg_2_frm_size___frm_size <= reg_wr_data_i[15:0]; // assigning to output assign frm_size_o = reg_2_frm_size___frm_size; // assigning to read data always_comb begin reg_2_frm_size_read = 32'h0; reg_2_frm_size_read[15:0] = reg_2_frm_size___frm_size; end // ****************************************** // Register FRM_CNT // ****************************************** logic [31:0] reg_3_frm_cnt_read; // assigning to output // assigning to read data always_comb begin reg_3_frm_cnt_read = 32'h0; reg_3_frm_cnt_read[31:0] = frm_cnt_i; end // ****************************************** // Reading stuff // ****************************************** logic [31:0] reg_rd_data = 32'h0; always_ff @( posedge reg_clk_i or posedge reg_rst_i ) if( reg_rst_i ) reg_rd_data <= 32'h0; else if( reg_rd_en_i ) begin case( reg_addr_i ) 8'h0: begin reg_rd_data <= reg_0_main_read; end 8'h1: begin reg_rd_data <= reg_1_ip_dst_read; end 8'h2: begin reg_rd_data <= reg_2_frm_size_read; end 8'h3: begin reg_rd_data <= reg_3_frm_cnt_read; end default: begin reg_rd_data <= 32'h0; end endcase end assign reg_rd_data_o = reg_rd_data; endmodule
      
      









ご覧のように、アイデアはうまくいきました:簡単なテキストの説明から本格的なモジュールが登場しました。手作業で仕上げる必要はありません-すぐに生産に移すことができます)



管理インターフェースとして、類似性Avalon-MMが使用されました



まとめ



ほんの数日前に、Ginhubで実装された 1Gおよび10G MACコアとUDP / IPスタックを見たときに、 Jinja2に出会いました。 ちなみに、それはうまく書かれていましたが、私はかなり表面的に、シミュレーションで見えました。



著者は、Jinja2を使用して、たとえばNポートマルチプレクサーAXI4-Streamなどのさまざまなモジュールを生成します。 このマルチプレクサは、この記事の冒頭で書いたものよりもはるかに複雑です。



私はcsr_mapを生成するためのスクリプトを急いで投げて、 Jinja2の機能を感じました(ただし、その一部を感謝していると思います)。コード。



もちろん、このスクリプトは未加工であり、まだ開発には使用していません(さまざまな理由でIPコアの美しいアーキテクチャが美しいアーキテクチャのままであることがあるため、使用するかどうかさえわかりません)。



ソースコードをgithubに完全にアップロードしました。 このテンプレートが誰かに役立つ場合、私は喜んでいます:リクエストに応じて改善するか、誰かのプルリクエストを受け入れる準備ができています)



ご清聴ありがとうございました! 質問がある場合は、間違いなく質問してください。



非表示のテキスト
もちろん、FPGAハブが時計に移動されたことは残念です。




All Articles