Showing posts with label SystemVerilog. Show all posts
Showing posts with label SystemVerilog. Show all posts

Thursday, October 10, 2019

Systemverilog simulators related

performance profile

VCS:
    profile in VCS by time or memory.
    #for memory profiling
        -simproile    //compile option
        -simprofile mem    //simulation option
    #for time profiling
        -simprofile    //compile options
        -simprofile time    //sim options
    the profile report will be store at compile directory named "profilereport"

IUS:
    debug sim hang by using cpu usage report
    compile option: -linedebug
    simulation option: -profile
    at sim hang point, stop test by: Ctrl+c (1 time), then ncsim>exit
    check the ncprof.out file (cpu usage summary and source code location)

Coverage

code coverage fefinition:
line/statement: will not cover module/end module/comments/time scale
block: begin...end, if...else, always
expression:
branch: case
conditional: if...else,  ternary operator (?:)
toggle:
FSM:

VCS:
%vcs -cm line+tgl+branch source.v
%simv -cm branch

vcs urg (Unified Report Generator):
%urg -dir simv1.vdb [simv2.dir simv3.vdb ...] -metric line+cond+branch -report specified_ouput_dir    //general options
%urg ... -parallel -sub bsub -lsf ""    //run urg in parallel to speed up
%urg -elfile <filename>    //for exclusion files
%dve -covdir simv.vdb//view coverage db directly in DVE

Dump Waveform

1. Options
setenv FSDB_FORCE    //to display forced signals in highlight in waveform viewer
2. sdf
3. use do file to control fsdb dump.
%vcs -ucli -do PATH_OF_DO_FILE    //simulation options
below is a sample tcl do file:
####start of file###############
#control fsdb dump
set run_time_before_dump 0us
set dump_all 1
set run_time 400us
run $run_time_before_dump
set TOP eth_top_tb
fsdbDumpfile $TOP.fsdb
if (dump_all == 1) {
    fsdbDumpvars 3 $TOP
    fsdbDumpvars 0 $TOP.xxx...
    fsdbDumpMDA 1 $TOP...
} esel {
    ...
}
run $run_time
exit
####end of file################
4.dump force information
simv +fsdb+force
5.dump glitch info
Before VCSMX/1509, 
simv +fsdb+sequential +fsdb+glitch=0 +fsdb+region
+fsdb+glitch=num,0表示所有的glitch都保存,1表示最近的glitch保存,2表示最近两个glitch被保存 
After VCSMX/1509, 
simv +fsdb+delta

Race Condition

VCS:
+evalorder    //vcs sim option
                     //eval combinational group then behavioral group.
                     //reduce race, refer to vcs userguide

Tuesday, August 27, 2019

Digital Design Verification Subsystem Lessons Learned


  1. make bus randomized during invalid cycle, to catch dut bugs that did not check valid signals. However, the constraint should still give 0 bus value a weight, so that 0 bus value can happen in invalid cycle, just in case that in real chip, the bus value is gated to 0 by upper layer module.
  2. bugs of features that cross two subsystems are difficult to catch. if a value generated in one module, and will be used in next module, it has a higher chance to have bug uncatched, as it needs correct model behavior as real hardware. solution: a) it's better to try to implement this in the same module, instead of split it into different modules, if possible. an example is the idle insertion/deletion to accommodate the AM (alignment marker) insertion/deletion in PCS. in this case, it's better to implement it in PCS instead of in MAC.
  3. bugs of features that related to performance AND cross two subsystems are extremely difficult to catch. possible solutions: a) in subsystem level, force to speed up counter value count, and increase pkt counts and so on, so that the possibility is increased. however, this needs very specific test scenarios; b) use hardware acceleration or e, e.g. Palladium or Synopsys ZeBu; c) chip top test to simulate the cross subsystem behaviors
  4. simplify architecture based on real application. sometimes, smart design means simple brutal force. this one needs the architects sensitivity to the industry use scenarios. I have two examples: a) initially we design our switch to have all kinds of protocols and features to both be legacy device compatible and also includes new features. this leads to overdesign and many bugs (insufficient man power and verification time). however, data center ethernet switch are more focused on feed and speed, it needs high throughput, less focused on protocols. end results, a lot of features are not used, a lot of hot new architectures (like SDN) are not used. this is big waste of resources. b) ...?
  5. Review every registers with DE in review meetings. Need to decide for every register: a) what is its meaning and actual use case (e.g. how does software guy config it, how does SA test it in real chip); b) can it be randomized in base test or should be tested in specific test; c) does it have some values that are often used by SA and Customer in real chip? weighted distribution?
  6. Review base config randomization constraints for base test. this is also related to 5) as some of the configs are registers. it needs designer and SA's input to confirm.
  7. simulation time vs packets number: Do Not trade packets number for simulation time. Meaning that do not try to save hardware resources. For verification, the first priority is function correctness, and the more packet number, the more possible that a bug will be hit. CPU resources are cheap, real chip bugs are expensive!
  8. random noise register/memory access during every test. But make sure that the noise and actually traffic can actually hit the same register/memory to trigger corner bugs.
  9. there should be 2 types of checker for a features if it cannot be accurately checked in every scenarios: a) a specific test that accurately check it's function; b) a general checker which is enabled in every testcase, and act as a sanity checker, in case there is some fundamental bugs in certain corner cases, which was not found in the a).
  10. choose the constraint range carefully, and choose the random value carefully. speed mode, pkt number, and the event happen time are all related, need to consider them when setting constraint or randomization range.
  11. have status and counters for monitors and controlling tb in cfg or virtual interface. for example, have counter count received pkt count, or have status variable to monitor dut is in transmiting or idle state, and so on.

Monday, July 15, 2019

UVM Notes

This article is to list down the most used constructions of UVM to my personal understanding.

UVM simulator steps

  • VCS(Synopsys)
  • IUS(Cadence)
  • Questa(Mentor)
1. set $UVMHOME to instal dir of required UVM library
2. Use irun option -uvmhome to reference $UVMHOME
3. Use incdir to reference any included file directories
%irun -f run.f

run.f:
-uvmhome $UVMHOME
-incdir .../sv
.../sv/tb_pkg.sv
top.sv

top.sv:
module top();
    import uvm_pkg::*;
    import tb_pkg::*;
    initial begin
        //run test
        ...
    end
endmodule: top

Debugging case: 
when using 3 step compile on vcs, vlogan and elab must both add uvm_dpi.cc to actually compile the c. add the file in file list does not work. 
if using 1 step compile, then just need to include the file in filelist. 


UVM Directory Structure

contains two kinds of code:

UVM_ROOT UVM_TOP

Tips: (1) debug features
uvm_top.print_topology();  //advised to be called from end_of_elaboration_phase

=======================================
If you look into the uvm source code, run_test() task is actually a task defined in the class uvm_root. it's the implicit top-level and phase controller for all UVM components. the UVM automatically creates a single instance of <uvm_root> that users can access via the global (uvm_pkg-scope) variable uvm_top. 
  • long time confusion solved: the run_test() called in top tb module is defined in uvm_globals.svh which actually calls the run_test() in uvm_root.
  • uvm_test_top is not a variable in uvm_root, how can you access that with uvm_root?
  • class uvm_root extends uvm_component
  • const uvm_root uvm_top = uvm_root::get();
  • uvm_top is the top-level component, and any component whose parent is specified as NULL becomes a child of uvm_top.
  • uvm top manages the phasing for all components.
  • set globally the report verbosity, log files, and actions(?).
  • Because uvm_top is globally accessible (in uvm_pkg scope(?)), UVM's reporting mechanism is accessible from anywhere outside uvm_component, such as in modules and sequences.

UVM Configuration

1) uvm_config_db

  • uvm_config_db#(int)::set(this, "env.agent", "is_active", UVM_PASSIVE);
  • uvm_config_db#(int)::set(null, "uvm_test_top.env.agent", "is_active", UVM_PASSIVE);
  • uvm_config_db#(int)::set(uvm_root::get(), "uvm_test_top.env.agent", "is_active", UVM_PASSIVE);//equivalent to using null
  • uvm_config_db#(int)::set(null, "*.env.agent", "is_active", UVM_PASSIVE);
  • uvm_config_db#(int)::set(null, "uvm_test_top.env*", "is_active", UVM_PASSIVE);
  • Database must be type parameterized. this allows config db to be created for any standard or user-defined type; and allows better compile time checking.
  • Methods are static
  • Methods use a specific contxt argument, whi is usually this; unless the set is called from the top module, in which case it must be assigned to null
  • syntax: static function void set(uvm_component cntxt, string inst_name, string field_name, ref T value)
  • syntax: static function void get(uvm_component cntxt, string inst_name, string field_name, ref T value)
    • get is only required when set is called from the top level module or outside the build phase
  • syntax: static function bit exists(uvm_component cntxt, string inst_name, string field_name, bit spell_chk = 0)
  • syntax: static task wait_modified(uvm_component cntxt, string inst_name, string field_name)
  • inst_name may contain wildcards or regular expression syntax
  • for object, interfaces or user-defined types, use uvc_config_db
  • for run-time configuration, use uvc_config_db


2) a specialized cfg task is set_config_* for uvm_component class, where * can be int, string or object, depending on type of config property:

  • config settings are automatically resolved in UVM build phase; that is because apply_config_settings() is executed in the build phase (when super.build_phase() is called in any uvm_component class). settings are applied only when match is found, if not found, field names will be unset, and mismatched configuration set's should be listed at end of simulation.
  • syntax: virtual function void set_config_in (string inst_name, string field, bitstream_t value)
    • inst_name is relative pathname to a specific component instance from the component where the method is called
    • field is a string containing a config property name of the instance class
    • set build options before calling super.build_phase; this is also why the parameters are strings, because the components and fields does not exist yet.
    • set_config_int("env.agent", "is_active", UVM_PASSIVE);
    • set_config_int("*", "recording_detail", 1);//default is 0, by enabling the recording details for every component, transactions can be viewed in the waveform window(?)
  • the creation of the agent instance in the parent build_phase() triggers the execution of the build_phase() of the agent instance.
  • config property must be automated in the component where declared (field registered)
  • Config settings in higher scope take precedence over lower scopes.
  • Config settings in the same scope conform to "last one in wins"

UVM Field Automation

#Non-array
1) `uvm_field_int (<field_name>, <flags>)
2) `uvm_field_object (<field_name>, <flags>)
3) `uvm_field_string (<field_name>, <flags>)
4) `uvm_field_event (<field_name>, <flags>)

#static Array:
1) `uvm_field_sarray_enum (<enum_type>, <field_name>, <flags>)
2) `uvm_field_sarray_int (<field_name>, <flags>)
3) `uvm_field_sarray_object (<field_name>, <flags>)
4) `uvm_field_sarray_string (<field_name>, <flags>)
#dynamic Array:
1) `uvm_field_array_enum (<enum_type>, <field_name>, <flags>)
2) `uvm_field_array_int (<field_name>, <flags>)
3) `uvm_field_array_object (<field_name>, <flags>)
4) `uvm_field_array_string (<field_name>, <flags>)
#dynamic Array:
1) `uvm_field_queue_enum (<enum_type>, <field_name>, <flags>)
2) `uvm_field_queue_int (<field_name>, <flags>)
3) `uvm_field_queue_object (<field_name>, <flags>)
4) `uvm_field_queue_string (<field_name>, <flags>)

#associative Array:
1) `uvm_field_aa_<d_type>_<ix_type>

#flags
UVM_NOVOMPARE

#the do_* functions are worth more discussion later. 

uvm_factory

b extends a, c extends a, override(a,c)
will b be affected?
Tips: (1) how to use factory debug features. 
uvm_factory::get().print(); //prints the uvm_factory details like registration and override. can be called from build phase, connect phase or mostly likely end_of_elaboration_phase. 

UVM Phasing

Tips:(1) phasing debug features (not very useful)
sim option +UVM_PHASE_TRACE
(2) objection debugging 
sim option +UVM_OBJECTION_TRACE

UVM_sequence

start_item()
finish_item()
get_response()
driver: 
seq_item_port.get_next_item(), 
seq_item_port.item_done(), 
seq_item_port. put(resp)

UVM_POOL

uvm_object_string_pool #(T)
pool.get(string) #get object by a name

Scoreboard

A scoreboard normally consists of 3 components of functions:
1) reference model/transfer function
     c++, systemC or systemverilog: a) existing C model; b) create model, not in collaboration with design team( duplication of errors)
2) Data Storage
     model output instant, DUT takes time to output. Need to store data (and synchronization)
    Queue: output data in same order as input order
    Associative Array: out of order output. Key unique and knonw for input and output. herefore, key is either: a) untransformed port of data, or b) can be generated from data
3) comparison/check logic

Scoreboard internals:
    update counter, tracking received, dropped, matched, and mismatched
    report_phase, print summary of statistics
    end of simulation: check scoreboard queues are empty

Scoreboard must create a new copy of received data item by cloning, before writing the cloned packet to the queue.

UVM TLM Communication bwtween Components

TLM concepts: Port and Imp
    Data Flow: producer create data, consumer consume data
            Producer ---data---> Cosumer
    Control Flow: Initiator sends request to Target
            Initiator ---request---> Target
    producer is initiator: write operation, also called push/put: e.g. analysis connections
    producer is target: read operation, also called pull/get

Port: TLM connection object for Initiator
Imp (implementation): TLM connection object for target.
Export:
symbols: square(port), circle(imp), triangle(export)
port.connect(Imp)
port.connect(Export)

TLM Analysis Interface
uvm_analysis_port #(data type) ap_out
ap_out = new("ap_out", this);

`uvm_analysis_imp_decl(_foo)
uvm_analysis_imp_foo #(data type) foo_in
`uvm_analysis_imp_decl(_bar)
uvm_analysis_imp_foo #(data type) bar_in

function void write_foo(input ---);
endfunction
function void write_bar(input---);
endfunction

...ap_out.connect(...ap_in)


Complex Module UVC connection(external to intermal)
two ways:
1. Module monitor: all external connections are made to this monitor and monitor is responsible for routing connections to other component in the UVC.
the good:  Single, central location for connecting external TLM interfaces.
the bad: at the expense of additional internal interface connections to other components.

2. Module connections: external TLM interfaces are placed on UVC itself, then routed using hierarchical connections and not separate TLM interface. use of TLM export object
the good: fewer TLM connections
the bad: losing some readability

Port initiators can be connected to port, export, or imp targets.
Export initiators can be connected to export or imp targets.
Imp cannot be a connection initiator. Imp is a target only, and is always the last connections object on a route.

TLM FIFO

Analysis FIFO
uvm_tlm_analysis_fifo is a specialization of uvm_tlm_fifo:
unbounded (size=0)
analysis_export replaces put export, support analysis write method.

uvm_analysis_port ---> analysis_export---analysis_fifo---get_peek_export <---scoreboard_get_port

uvm_tlm_analysis_fifo #(data type) tb_fifo = new("...", this);
uvm_get_port $(data type) sb_in = new("...", this);
function void connect_phase();
    sb_in.connect(tb_fifo.get_peek_export)
endfunction

by the way, analysis fifo's blocking_get_export is just an alias to get_peek_export










Thursday, April 4, 2019

Systemverilog Notes



  • Systemverilog simulation steps
    • asdfasd
      • A net represent connections between hardware elements. Just as in real circuits, nets have values continuously driven on them by the outputs of devices that they are connected to.
      • can only be driven with a continuous assignment statement
      • a net is the only construct that resolves the effect of different states and strengths simultaneously driving the same signal.
      • the behavior of a net is defined by a built-in resolution function using the values and strengths of all the drivers on a netEvery time there is a change on one of the drivers, the function is called to produce a resolved value. The function is create at elaboration (before simulation starts) and is based on the kind of net type, wand, wor, tril, etc.
    • logic: 
      • can be driven by continuous assignments, gates, and modules, in addition to being a variables.
      • can be used anywhere a net is used, except that a logic variable cannot be driven by multiple structural drivers (such as when you are modeling a bidirectional buss)
    • continuous assignment, procedural assignment, and classes
      • class based testbenches cannot have continuous assignments because classes are dynamically created objects and are not allowed to have structural constructs like continuous assignments. 
      • Although a class can read the resolved value of nets, it can only make procedural assignments to variables. Therefore, the testbench needs to create a variable that is continuously assigned to a wire (if you want to have multiple drives to that wire).
      • procedural assignments to variables use the simple rule: last write wins. You are not allowed to make procedural assignemtns to nets because there is no way to represent how the value you assigning should be resolved with the other drivers.
    • assign/deassign, force/release
      • Another form of procedural continuous assignment is provided by the  force and  release procedural statements. These statements have a similar effect to the  assign - deassign pair, but a force can be applied to nets as well as to variables
      • A  force statement to a variable shall override a procedural assignment, continuous assignment or an assign procedural continuous assignment to the variable until a  release procedural statement is executed on the variable. 
      • A  force procedural statement on a net shall override all drivers of the net—gate outputs, module outputs, and continuous assignments—until a  release procedural statement is executed on the net. When released, the net shall immediately be assigned the value determined by the drivers of the net.
    • logic vs wire in an interface 
      • if your testbench drives an asynchronous signal in an interface with a procedural assignment, the signal must be a logic typeSignals in a clocking block are always synchronous and can be declared as logic or wire(?).
      • wire can resolve multiple structural drivers, but logic cannot. choose depending on your user scenarios.
    • procedures and procedural assignment
      • initial_construct ::=  initial statement_or_null
      • always_construct ::= always_keyword statement
      • always_keyword ::=  always |  always_comb |  always_latch |  always_ff
      • final_construct ::=  final function_statement
      • function_declaration ::=  function [ lifetime ] function_body_declaration
      • task_declaration ::=  task [ lifetime ] task_body_declaration
      • In addition to these structured procedures, SystemVerilog contains other procedural contexts, such as coverage point expressions, assertion sequence match items, and action blocks.
  • define and parameters for constant
  • Assertions
  • Modules and Hierarchy
    • Bind
      • Binding is like secretly instantiating a module/interface within another RTL file without disturbing the existing code. The binded module/interface is instantiated directly into the target module. How to bind inner signals from DUT?
  • OOP
    • singleton classes
      • The singleton pattern is implemented by creating a class wit a method that creates a new instance of the class if one does not exist. If an instance already exists, it simply returns a handle to that object. To make sure that he object cannot be instantiated any other way, you must make the constructor protected. Don't make it local, because an extend class might need to access the constructor.
  • Number
    • Rules for expression types (from LRM). The following are the rules for determining the resulting type of an expression:
      • — Expression type depends only on the operands. It does not depend on the left-hand side (if any).
      • Decimal numbers are signed.
      • — Based numbers are unsigned, except where the s notation is used in the base specifier (as in 4'sd12 ).
      • Bit-select results are unsigned, regardless of the operands.
      • Part-select results are unsigned, regardless of the operands even if the part-select specifies the entire vector.
        • logic [15:0] a;
        • logic signed [7:0] b;
        • initial
        • a = b[7:0]; // b[7:0] is unsigned and therefore zero-extended
      • Concatenate results are unsigned, regardless of the operands.
      • — Comparison and reduction operator results are unsigned, regardless of the operands.
      • — Reals converted to integers by type coercion are signed
      • — The sign and size of any self-determined operand are determined by the operand itself and independent of the remainder of the expression.
      • — For non-self-determined operands, the following rules apply:
      • — If any operand is real, the result is real.
      • If any operand is unsigned, the result is unsigned, regardless of the operator.
      • If all operands are signed, the result will be signed, regardless of operator, except when specified otherwise.

Friday, March 1, 2019

Systemverilog defines for use in variable names


How do form Variable names by using defines in system verilog:

https://stackoverflow.com/questions/20759707/how-do-form-variable-names-by-using-defines-in-system-verilog

https://verificationacademy.com/forums/systemverilog/define-macros-usage

https://stackoverflow.com/questions/15373113/how-to-create-a-string-from-a-pre-processor-macro


Below is from LRM:

The `define macro text can also include `" , `\`" , and ``.
An `" overrides the usual lexical meaning of " and indicates that the expansion shall include the quotation mark, substitution of actual arguments, and expansions of embedded macros. This allows string literals to be constructed from macro arguments.
A mixture of `" and " is allowed in the macro text, however the use of " always starts a string literal and
must have a terminating " . Any characters embedded inside this string literal, including `" , become part of the string in the replaced macro text. Thus, if " is followed by `" , the " starts a string literal whose last character is ` and is terminated by the " of `" .

A `\`" indicates that the expansion should include the escape sequence \" . For example:
`define msg(x,y) `"x: `\`"y`\`"`"
An example of using this `msg macro is:
$display(`msg(left side,right side));
The preceding example expands to:
$display("left side: \"right side\"");

A `` delimits lexical tokens without introducing white space, allowing identifiers to be constructed from arguments. For example:
`define append(f) f``_master
An example of using this `append macro is:
`append(clock)
This example expands to:
clock_master

The `include directive can be followed by a macro, instead of a string literal:
`define home(filename) `"/home/mydir/filename`"
`include `home(myfile) 



i paraphrase from the above link, and pay attention to the so called token identifier in systemverilog:


Quote:

So this means if i just use ``I , the expansion of macro `abc(1,a) would be :
assign abc[1] == R.duI_clk_x . Since its not defined as a separate token ??


Yes, that would be the result.

Quote:

Both R and I are arguments to the macro so why you say that no ``R`` is needed where as ``I`` is needed. Is it because the left hand side has no dependency on R or is it because R is not breaking the lexical variable ?

Correct again. What you call a lexical variable is what the compiler calls a token identifier. The compiler grabs text in chunks called tokens, before it knows what the identifier is (variable, typedef, module name). An identifier starts with a letter, followed by any number of alpha-numeric characters, as well as _(underscore). Any other character ends the token.


Quote:

So here ``I`` is not used, why ?

Only [I] is needed because I is surrounded by characters that are not part of token identifiers.



I think the issue comes when the macros are used with generate loop.

The complete code is :

`define WB_DUT_U_ASSIGN(phy_i,idx)\

assign b[phy_i] = `DUT_PATH.Ilane``idx``.a;\


genvar wb

generate

for(wb=0;wb<8;wb++) begin:wb_a

`WB_DUT_U_ASSIGN(wb,wb)

end

endgenerate



Macros are preprocessor directives. There are expanded before parsing any Verilog/SystemVerilog syntax.

In reply to kvssrohit: As I said before, macros get expanded as text for any generate processing. `dev_(i) gets expanded as top.i.XXX

The only way to achieve what you want withing SystemVerilog is to restructure your instance names into an array and access them with proper indexing. top[i].


Otherwise there are other macro processing tools that you can use to generate the identifier names you are looking for. But that becomes quite a maintenance headache.

genvar i;
for (i=0; i<=31; i=i+1) begin: gen_force
initial
force top[i].zzz = 1'b1;
end

I don't think you will be able to do mix compiler time defines with run-time variables. There is no space/tab to distinguish between 2 lexical tokens(VAR ,str_var). When you put space the expression will become illegal.

You can use parameterized macro but you cannot use a variable while calling it.


Tuesday, March 1, 2016

Verilog/Systemverilog begin/end pairs matching (Matchit Plugin for VIM)


Source: http://blog.edmondcote.com/2011/05/vim-setup-for-systemverilog.html

When configured correctly, match_it.vim allows the % key to be configured to match more than just single characters. In the context of SystemVerilog, we can enable the user to move the cursor between Verilog-style statements that define blocks of code (e.g. begin/end, class/endclass, package/endpackage, etc.)

Configuring the plugin is pretty straightforward. I started with the configuration posted at http://vblog.learn-asic.com/?p=40 and made some minor tweaks (make sure to use the tick character ', not `, ‘, or ’ when defining b:match_words).


filetype plugin on
source ~/.vim/plugin/matchit.vim
if exists('loaded_matchit')
let b:match_ignorecase=0
let b:match_words=
\ '\<begin\>:\<end\>,' .
\ '\<if\>:\<else\>,' .
\ '\<module\>:\<endmodule\>,' .
\ '\<class\>:\<endclass\>,' .
\ '\<program\>:\<endprogram\>,' .
\ '\<clocking\>:\<endclocking\>,' .
\ '\<property\>:\<endproperty\>,' .
\ '\<sequence\>:\<endsequence\>,' .
\ '\<package\>:\<endpackage\>,' .
\ '\<covergroup\>:\<endgroup\>,' .
\ '\<primitive\>:\<endprimitive\>,' .
\ '\<specify\>:\<endspecify\>,' .
\ '\<generate\>:\<endgenerate\>,' .
\ '\<interface\>:\<endinterface\>,' .
\ '\<function\>:\<endfunction\>,' .
\ '\<task\>:\<endtask\>,' .
\ '\<case\>\|\<casex\>\|\<casez\>:\<endcase\>,' .
\ '\<fork\>:\<join\>\|\<join_any\>\|\<join_none\>,' .
\ '`ifdef\>:`else\>:`endif\>,' .
\ '`ifndef\>:`define\>:`endif\>'
endif

raspberry pi gpio controls

#gpiozero library https://gpiozero.readthedocs.io/en/stable/#