线程使用、控制、通信

线程的使用

  • module(模块)作为SV从Verilog继承过来的概念,自然地保持了它的特点,除了作为RTL模型的外壳包装和实现硬件行为,在更高层的集成层面,模块之间也需要通信和同步
  • 对于硬件的过程块,它们之间的通信可理解为不同逻辑/时序块之间的通信或者同步,是通过信号的变化来完成的。
  • 从硬件实现的角度来看,Verilog通过always,initial过程语句块和信号数据连接实现进程间通信。
  • 我们可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)、等待特定事件(时钟周期)或者时间(固定延时)来完成。
  • 如果按照软件的思维理解硬件仿真,仿真中的各个模块首先是独立运行的线程(thread) 。
  • 模块(线程)在仿真一开始便并行执行,除了每个线程会依照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间的信号变化来完成模块之间的线程同步。

线程的概念

  • 线程即独立运行的程序。线程需要被触发,可以结束或者不结束。
  • 在module中的initial和always,都可以看做独立的线程,它们会在仿真0时刻开始,而选择结束或者不结束。
  • 硬件模型中由于都是always语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为它们并不会结束
  • 软件测试平台中的验证环境都需要由initial语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此软件测试端的资源占用是动态的。
  • 软件环境中的initial块对语句有两种分组方式,使用begin...end或fork...join。begin...end中的语句以顺序方式执行,而fork...join中的语句则以并发方式执行。fork...join类似的并行方式语句还包括fork...join_any,fork...join_none。
  • 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程。父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程。当子线程终止时,父线程可以继续执行。当父线程终止时,其所开辟的所有子线程都应当会终止。

线程的控制

fork  join_any 某个子线程执行完退出后,剩下的子线程还会执行

fork join_none不会等待执行子线程,(点火后)直接执行后面线程外语句

fork
    $display();  //执行这条后就会跳出,执行外面的display
    #10 $display();  //10个单位后会执行
    #20 $display();
join_any 
    $display();

在sV中,当程序中的initial块全部执行完毕,仿真器就退出了。如果我们希望等待fork块中的所有线程执行完毕再退出结束initial块,我们可以使用wait fork语句来等待所有子线程结束。

fork
    check_trans(tr1);
    check_trans(tr2);
    ……
join_none
……
wait fork;   

使用fork……join_any或者fork……join_none以后,可以使用disable来指定需要停止的线程。

fork:time_block
    begin
        wait(bus.cb.addr==tr.addr);
        $display("@$0t:Addr match %d",$time,tr.addr);
    end
    #TIME_out $display();
join_any
disable timeout_block;

disable fork可以停止从当前线程中衍生出来的所有子线程

initial begin
    check_trans(tr0); //线程0
    //创建一个线程来限制disable fork的作用范围
    fork //线程1
        begin    
            check_trans(tr1); //线程2
            fork //线程3
                check_trans(tr2); //线程4
            join
            //停止线程1-4,单独保留线程0
            #(TIME_OUT/2)disable fork;  //停止所在的线程
        end
    join
end

如果给任务或者线程指明标号,那么线程被调用多次以后,如果通过disable去禁止这个线程标号,所有衍生的同名线程都将被禁止。

task wait_for_time_out(int id);
    if(id==0)
     fork
      begin
       #2;
       disable wait_for_time_out;
      end      
     join_none
      fork:just_a_little
        begin
         ……
        end
      join_none
endtask

//任务wait_for_time_out被调用了三次,从而衍生出三个线程
//线程0在#2延时之后禁止了该任务,而由于三个线程均是同名线程,因此这些线程都被禁止
initial begin
    wait_for_time_out(0);
    wait_for_time_out(1);
    wait_for_time_out(2);   //0时刻三个都会执行完,都为fork_join_none。任务退出,线程还在
    #(TIME_OUT)
end

线程间的通信

  • 测试平台中的所有线程都需要同步并交换数据。
  • 一个线程需要等待另一个。
  • 多个线程可能同时访问同一-个资源。
  • 线程之间可能需要交换数据。
  • 所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication)。

event事件

  • Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待事件的变化。
  • 其它线程可以通过->操作符来触发事件,结束对第一个线程的阻塞。

三个线程里唯一一个不用new的,可以将其当作一个对象,按类处理。

event e1,e2;
initial begin
    $display("@%0t:1:before trigger",$time);
    ->e1;
    @e2;   //改为wait(e2.triggerd())
    $display("@%0t:1:after trigger",$time);
end
initial begin
    $display("@%0t:2:before trigger",$time);
    ->e2;
    @e1;   
    $display("@%0t:2:after trigger",$time);
end

//第一个初始块启动,触发e1事件,然后阻塞在e2上。第二个初始化启动触发e2事件,然后阻塞在e1上。
总会有一个after trigger没有执行
//e1和e2在同一时刻被触发,由于delta cycle的时间差使两个初始化模块无法等到e1或e2
更安全的方式是使用event的方法triggered(),那么全部都会打印出来
  • 可以使用电平敏感的wait (e1. triggered () )来替代边沿敏感的阻塞语句@e1。
  • 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止。
  • 这个方法比起@而言,更有能力保证,只要event被触发过,就可以防止引起阻塞
//通过wait进行线程通知
class car;
    bit start=0;            //event e_start;
    task launch();
        start=1;            //->e_start
        $display();
    endtask
    task move();
        wait(start==1);     //wait(e_start.triggerd)  可以实现同样的需求
    endtask;
    task driver();
        fork 
         this.launch();   //虽然是并行语句,但由于wait会导致luanch先运行
         this.move();
        join
    endtask
endclass
//使用@的情况,不能使用wait triggered,因为边沿触发可以一直触发,而电平触发只能触发一次
//uvm中的triggered不同,是可以擦除状态的
class car;
    event speed;
    int speed=0;
 task speedup();
    #10ns;
    ->e_speedup;
 endtask
 task display();
    forever begin
      @e_speedup;   //一直在等待
      speed++ 
      $display("speed is %0d";speed);
    end
 endtask
module road;
initial begin
    automatic car byd=new();
    byd.speedup();  //1
    byd.speedup();  //2
    byd.speedup();  //3
end
endmodule

semaphore旗语

  • semaphore可以实现对同一资源的访问控制。
  • 对于初学者而言,无论线程之间在共享什么资源,都应该使用semaphore等资源访问控制的手段,以此避免可能出现的问题。
  • semaphore有三种基本操作。new( )方法可以创建一个带单个或者多个钥匙的semaphore,使用get( )可以获取一个或者多个钥匙,而put( )可以返回一个或者多个钥匙。
  • 如果你试图获取一个semaphore而希望不被阻塞,可以使用try_ get()函数。它返回1表示有足够多的钥匙,而返回0则表示钥匙不够。
program automatic test(bus_ifc.TB bus);
    semaphore sem;  //创建一个semphore
    initial begin
        sem=new(1);  //分配一个钥匙
        fork
         sequencer();  //产生两个线程  这里用semphore是为了让只有一个能够进行,new参数为1
         sequencer();
        join
    end
    task sequencer;
        repeat($urandom%10)   //随机等待0-9个周期
        @bus.cb;
        sendTrans();  //执行总线事务
    endtask
    task sendTrans;
        sem.get(1);   //获取总线钥匙
        @bus.cb;    
        bus.cb.addr<=t.addr;  //信号驱动到总线
        ……
        sem.put(1);   //处理完成后把钥匙返回,要不然会死锁
    endtask 
endprogram

资源共享的需求

  • 对于线程间共享资源的使用方式,应该遵循互斥访问(mutexaccess)原则。
  • 控制共享资源的原因在于,如果不对其访问做控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象称之为"线程不安全"。
//与前面事件的例子相比,前面是顺序,通过event设置了哪个先哪个后。而在这不知道顺序,
//虽然两者的结果都是顺序执行
class car;
    semphore key;
    function new();
        key=new(1);
    endfunction
    task get_on(string p);
        $display("%s is waiting for the key",p);
        key.get();
        #1ns;
        $display("%s got on the car",p);
    endtask
    task get_off(string p);
        $display("%s is waiting for the key",p);
        key.put();
        #1ns;
        $display("%s returned the key",p);
    endtask
endclass
    
module family;
    car byd=new();
    string p1="husband";
    string p2="wife";
    initial begin
        fork
         begin
            byd.get_on(p1);
            byd.get_off(p1);
         end
         begin
            byd.get_on(p2);
            byd.get_off(p2);
         end
        join
    end
endmodule
  • key在使用前必须要做初始化,即要告诉用户它原生自带几把钥匙。
  • 没有在semaphore: :get( )/ put( )函数中传递参数,即默认他们等待和归还的钥匙数量是1。
  • semaphore可以被初始化为多个钥匙,也可以支持每次支取和归还多把钥匙用来控制资源访问。
  • seamphore可以初始化为0,通过put可以放入钥匙。
//通过类实现同样功能
class carkeep;
    int key=1;
    string q[$];
    string user;
    task keep_user();
     fork
        forever begin; //管理分发钥匙
         wait(q.size()!=0 && key!=0);
         user=p.pop_front();
         key--;
        end
     join_none;
    endtask
    task get_key(string p);
     q.push(p); 
     wait(user==p);
    endtask
    task put_key(string p);
     if(user==p)begin
        key++;
        user="none";
     end
    endtask
endclass
class car;
 carkeep keep;
 function new();
  keep=new();
 endfunction  
 task driver();
  keep.keep_car();
 endtask
 task get_on(string p);
    $display("%s is waiting for the key",p);
    keep.get_key();
    #1ns;
    $display("%s got on the car",p);
  endtask
 task get_off(string p);
    $display("%s got off the car",p);
    keep.put_key(p)
    #1ns;
    $display("%s returned the key",p);
 endtask
endclass

maibox信箱

  • 线程之间如果传递信息,可以使用mailbox。mailbox和队列queue有相近之处。
  • mailbox是一种对象,因此也需要使用new( )来例化。例化时有一个可选的参数size来限定其存储的最大数量。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
  • 使用put( )可以把数据放入mailbox,使用get( )可以从信箱移除数据。
  • 如果信箱为满,则put( )会阻塞;如果信箱为空,则get( )会阻塞
  • peek( )可以获取对信箱里数据的拷贝而不移除它。
  • 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞(try)方法,即哪些是立即返回的,而哪些可能需要等待时间的。
program automatic bounded;
    mailbox mbx;
    initial begin
      mbx=new(1);  //容量为1
      fork
        for(int i=1;i<4;i++)begin
         $display("Producer:before put(%0d)",i);
         mbx.put(i);
         $display("Producer:after pur(%0d)",i);
        end
        repeat(4)begin
            int j;
            #1ns mbx.get(j);
            $display("Consumer:after get(%0d)",j);
         end
      join
    end
endprogram

//结果
Producer:before put(1)
Producer:after put(1)
Producer:before put(2)
Consumer: after get(1)
Producer:after put(2)
Producer:before put(3)
Consumer: after get(2)
Producer:after put(3)
Consumer: after get(3)

数据通信的需求

        如果我们继续通过上面这辆BYD,来模拟不同传感器(线程)到车的中央显示的通信,可以利用SV的mailbox (信箱)来满足多个线程之间的数据通信。

//分别用maibox和队列实现
class car;
    mailbox tmp_mb,spd_mb;        //int tmp_q[$],spd_q[$];
    int sample_period;
    function new();
      sample_period=10;
      tmp_mb=new();
      spd_mb=new();
    endfunction
    task sensor_tmp;
      int tmp;
        forever begin
          std::randomize(tmp) with {tmp>=80 && tmp<=100;}
          tmp_mb.put(tmp);  //类里任务调用任务        //tmb_q.push_back(tmp)
          #sample_period;
        end
    endtask
    task sensor_spd;
      int spd;
      forever begin
        std::randomize(spd) with {spd>=50 && spd<=60};
        spd_mb.put(spd);                   //spd_q.push_back(spd);
        #sample_period;
      end
    endtask
    task driver();
      fork
        sensor_tmp();
        sensor_spd();
        display(tmp_tb,"temperature");
        display(spb_tb,"speed");
      join_none
    endtask
    task display(mailbox mb,string name="mb");  //(string name,ref int q[$])
      int val; 
      forever begin
        mb.get(val);                            //wait(q.size()>0);
        $display("car::%s is %0d",name,val);    //val=q.pop_front();  使用队列记得加上wait
      end 
    endtask
endclass
module road;
    car byd=new();
    initial begin
        byd.driver();
    end
endmodule

maibox与queue在使用时的差别:

  • maibox必须通过new( )例化,而队列只需要声明,不用初始化。
  • mailbox可以将不同的数据类型同时存储,不过这么做是不建议的;对于队列来讲,它内部存储的元素类型必须一致。
  • maibox的存取方法put( )和get( )是阻塞方法(blocking method),即使用它们时,,方法不一定会立即返回,而队列所对应的存取方式,push_ back( )和pop_ front( )方法是非阻塞的,会立即返回。因此在使用queue取数时,需要额外填写wait(queue.size0) > 0)才 可以在其后对非空的queue做取数的操作。此外也应该注意,如果要调用阻塞方法,那么只可以在task中调用,因为阻塞方法是耗时的:而调用非阻塞方法,例如queue的push_ back( )和pop_ front(), 则既可以在task又可以在function中调用。
  • mailbox只能够用作FIFO,而queue除了按照FIFO使用,还有其它应用的方式例如LIFO (Last In First Out)
  • 对于mailbox变量的操作,在传递形式参数时,实际传递并拷贝的是mailbox的指针(所以这里没有标明方向或者使用ref);而在第二个例子中的task dispiay(),关于queue的形式参数声明是ref方向,因为如果采用默认的input方向,那么传递过程中发生的是数组的拷贝,以致于方法内部对queue的操作并不会影响外部的gueue本身。因此在传递数组时,读者需要考虑到,对数组做的是引用还是拷贝,进而考虑端口声明的方向。

mailbox的其它特性

  • mailbox在例化时,通过new(N)的方式可以使其变为定长(fixed length)容器。这样在负载到长度N以后,无法再对其写入。如果用new()的方式,则表示信箱容量不限大小。
  • 除了put()/get()/peek()这样的阻塞方法,用户也可以考虑使用try_ put()/try_ get()/try_peek()等非阻塞方法。
  • 如果要显式地限定mailbox中元素的类型,可以通过mailbox #(type = T)的方式来声明。例如上面的三个mailbox存储的是int,则可以在声明时进一步限定其类型为mailbox # (int)。

将stall和park两个线程的同步视作,先由stall发起同步请求,再等待park线程完成并响应同步请求,最后由stall线程继续其余的程序,最终结束熄火的过程。用之前掌握的SV三种进程通信的方式event、semaphore和mailbox来解决进程间的同步问题。

//所谓的同步其实是握手
class car;
    event e_stall;
    event e_park;
    task stall;
      #1ns;
      -> e_stall;  //2
      @e_park;     //3
    task park;
      @e_stall;   //1
      #1ns;
      ->e_park     //4
    endtask
    task driver();
        this.stall();
        this.park();
endclass
//semphore和maibox   get\put顺序相同   两者都是为空时要先放数据
class car;
    semphore key;                      //mailbox mb;
    function new();  
      key=new(0);                      //mb=new(1);                                  
    endfunction

    task stall;                        //int val=0;
      #1ns;         
      key.put();  //1                 //mb.put(val);
      key.get();  //4                 //mb.get(val);
    endtask
    task park;                        //int val=0;
      key.get();  //2                 //mb.get(val);
      #1ns;
      key.put();  //3                 //mb.put(val);
    endtask
    task driver();
      fork
        this.stall();
        this.park();
      join_none
    endtask
endclass

正在进行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,我们把这种暂停状态叫阻塞进程阻塞,有时也成为等待状态或封锁状态。通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而处于阻塞状态进程排成多个队列。

上面都是线程A请求同步线程B,线程B再响应线程A的同步方式

如果要在同步(事件)的同时,完成一些数据传输,那么更合适的是mailbox,因为它可以用来存储一些数据; 而event和semaphore更偏向于小信息量的同步,即不包含更多的数据信息。

通信要素的比较和应用

event:最小信息量的触发,即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来用来做线程之间的同步
semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素。
mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑使用此元素。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C++中,可以使用线程来进行串口通信。通过使用一个线程来读取串口数据,并使用另一个线程来写入串口数据,可以实现并行的双向通信。 下面是一个示例代码,演示了使用线程进行串口通信的方法: ```cpp #include <iostream> #include <thread> #include <SerialPort.h> // 串口读取线程函数 void readThread(SerialPort& port) { char buffer[256]; while (true) { int bytesRead = port.read(buffer, sizeof(buffer)); if (bytesRead > 0) { buffer[bytesRead] = '\0'; // 将读取的数据添加字符串结束符 std::cout << "Received: " << buffer << std::endl; } } } // 串口写入线程函数 void writeThread(SerialPort& port) { while (true) { std::string message; std::cout << "Enter message to send: "; std::getline(std::cin, message); if (message == "quit") { break; } port.write(message.c_str(), message.length()); } } int main() { SerialPort port("COM1", 9600); // 打开COM1端口,波特率为9600 if (!port.isOpen()) { std::cout << "Failed to open port" << std::endl; return 1; } // 创建读取和写入线程 std::thread readT(readThread, std::ref(port)); std::thread writeT(writeThread, std::ref(port)); // 等待线程结束 readT.join(); writeT.join(); port.close(); // 关闭串口 return 0; } ``` 在上述示例中,readThread函数作为串口读取线程的入口点,使用循环读取串口数据并打印到控制台。writeThread函数作为串口写入线程的入口点,使用循环从用户输入获取消息并写入串口。 在主函数中,创建了一个SerialPort对象来打开指定的串口。然后,创建了一个读取线程和一个写入线程,并使用std::thread类来启动线程。最后,使用join()方法等待线程结束,并在程序结束前关闭串口。 通过使用线程进行串口通信,可以实现同时进行读写操作,提高通信的效率和响应速度。需要注意的是,在多线程环境下进行串口通信时,需要注意对共享资源(如串口对象)的访问控制,以避免线程安全问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值