3、Tcl语言基础(下):控制流、过程定义、文件操作
好,咱们接着聊Tcl。上一章我们把变量、列表、字符串这些基本功过了一遍。这一章,我要带你进入真正能“干活”的部分——控制流、过程定义和文件操作。
说白了,前面学的都是“材料”,这一章学的是“怎么用这些材料搭房子”。在仿真脚本里,你要根据不同的仿真条件走不同的分支,要循环跑多个测试用例,要把重复的代码封装成过程,还要读写文件来记录结果。这些,都是日常逃不掉的操作。
3.1 控制流:让脚本有“脑子”
控制流就是让脚本能“做决策”。没有控制流,脚本就是一串从头跑到尾的线性命令,遇到复杂场景就傻眼了。
3.1.1 if 条件判断
if 是最基本的判断结构。语法很简单,但有个坑我当年踩过——if 后面的 { 必须和 if 在同一行,否则Tcl会报错。
set sim_mode "functional"
if {$sim_mode eq "functional"} {
puts "运行功能仿真"
} elseif {$sim_mode eq "timing"} {
puts "运行时序仿真"
} else {
puts "未知仿真模式"
}
我的习惯: 写 if 的时候,我总会在条件表达式外面加一层花括号。比如 if {$x > 5} {,而不是 if $x > 5 {。为什么?因为花括号能防止变量被提前展开,避免一些奇怪的副作用。这个习惯帮我省了不少调试时间。
3.1.2 switch 多分支选择
当分支超过三四个的时候,if-elseif 写起来就有点啰嗦了。这时候 switch 更清爽。
set testcase "uart_test"
switch $testcase {
"uart_test" {
puts "运行UART测试"
source "scripts/uart_test.tcl"
}
"spi_test" {
puts "运行SPI测试"
source "scripts/spi_test.tcl"
}
"i2c_test" {
puts "运行I2C测试"
source "scripts/i2c_test.tcl"
}
default {
puts "未识别的测试用例: $testcase"
}
}
嗯,这里要注意:switch 默认是精确匹配。如果你想用通配符匹配,可以加 -glob 选项;想用正则表达式,加 -regexp。我在项目中经常用 -glob 来匹配一类测试用例,比如 test_*。
3.1.3 while 循环
while 循环适合“不知道要循环多少次,但知道什么时候停”的场景。比如等待仿真结束。
set sim_done 0
set timeout 1000
set count 0
while {!$sim_done && $count < $timeout} {
after 10 ;# 等待10毫秒
set sim_done [check_sim_status]
incr count
}
if {$sim_done} {
puts "仿真正常结束"
} else {
puts "仿真超时!"
}
我曾经踩过的坑: 写 while 循环一定要加超时保护!有一次我写了个死循环,忘了加退出条件,结果脚本跑了整整一个周末,把服务器资源吃光了。从那以后,我所有的 while 循环都会配一个计数器或者超时变量。
3.1.4 foreach 遍历循环
foreach 是我个人用得最多的循环。遍历列表、遍历文件、遍历测试用例……几乎天天用。
set test_list {uart_test spi_test i2c_test gpio_test}
foreach test $test_list {
puts "正在运行: $test"
source "scripts/${test}.tcl"
puts "$test 完成"
}
foreach 还有一个“多变量遍历”的用法,可以同时遍历多个列表:
foreach test_name $test_list expected_result $expected_list {
set actual [run_test $test_name]
if {$actual eq $expected_result} {
puts "$test_name: 通过"
} else {
puts "$test_name: 失败 (期望 $expected_result, 实际 $actual)"
}
}
3.2 过程定义:把重复代码“包起来”
写脚本最怕什么?最怕复制粘贴。今天复制一段,明天粘贴一段,后天改需求的时候,你就得满世界找那些复制粘贴的代码,一个一个改。累不累?
所以,把重复的逻辑封装成过程(procedure),是仿真脚本工程化的第一步。
3.2.1 proc 的基本用法
proc run_simulation {test_name {timeout 1000}} {
puts "启动仿真: $test_name"
puts "超时设置: ${timeout}ms"
# 这里放实际的仿真启动逻辑
# ...
return "PASS"
}
# 调用
set result [run_simulation "uart_test"]
set result [run_simulation "spi_test" 2000]
看到没?{timeout 1000} 这种写法表示给参数一个默认值。调用的时候可以不传这个参数,它会自动用1000。这个特性在写仿真脚本时特别实用——大部分测试用默认超时就行,个别慢的测试单独设置。
3.2.2 局部变量与全局变量
过程内部默认是局部作用域。想访问全局变量?用 global 声明一下。
set sim_log_file "simulation.log"
proc log_message {msg} {
global sim_log_file
set fp [open $sim_log_file a]
puts $fp "[clock format [clock seconds]]: $msg"
close $fp
}
log_message "仿真开始"
log_message "测试用例执行完毕"
我建议: 尽量少用全局变量。全局变量多了,脚本的耦合度就高了,改起来容易出问题。我一般会把全局变量集中放在一个配置文件中,用 source 加载,而不是散落在各个过程里。
3.2.3 过程返回值
过程默认返回最后一个命令的执行结果。你也可以用 return 显式返回。
proc compare_results {golden_file result_file} {
set golden [read_file $golden_file]
set result [read_file $result_file]
if {$golden eq $result} {
return 1 ;# 匹配
} else {
return 0 ;# 不匹配
}
}
if {[compare_results "golden.txt" "result.txt"]} {
puts "结果一致,测试通过"
} else {
puts "结果不一致,测试失败"
}
3.3 文件操作:读写数据
仿真脚本离不开文件操作。你要读测试向量、写日志、保存波形、生成报告……可以说,文件操作是仿真脚本的“输入输出口”。
3.3.1 打开与关闭文件
set fp [open "simulation.log" w] ;# w: 写入, r: 读取, a: 追加
puts $fp "仿真日志开始"
close $fp
这里有个细节:open 返回的是文件描述符(一个整数),不是文件名。后面所有操作都用这个描述符。
3.3.2 读取文件内容
# 逐行读取
set fp [open "test_vector.txt" r]
while {[gets $fp line] >= 0} {
puts "读取到: $line"
# 处理每一行数据
}
close $fp
# 一次性读取全部内容
set fp [open "config.txt" r]
set content [read $fp]
close $fp
puts "配置文件内容: $content"
核心要点: gets 命令读取一行,返回读取的字符数。如果返回 -1,说明到了文件末尾。这个返回值可以用来控制循环。
3.3.3 写入文件
set fp [open "report.txt" w]
puts $fp "测试报告"
puts $fp "=========="
puts $fp "测试用例: uart_test"
puts $fp "结果: PASS"
puts $fp "时间: [clock format [clock seconds]]"
close $fp
3.3.4 文件操作的综合示例
来个实际点的例子。假设我们要写一个脚本,读取测试向量文件,逐条运行仿真,然后把结果写入报告。
# 打开测试向量文件
set vec_fp [open "test_vectors.txt" r]
# 打开结果报告文件
set rpt_fp [open "test_report.txt" w]
puts $rpt_fp "仿真测试报告"
puts $rpt_fp "生成时间: [clock format [clock seconds]]"
puts $rpt_fp "----------------------------------------"
set pass_count 0
set fail_count 0
while {[gets $vec_fp line] >= 0} {
# 跳过空行和注释行
if {$line eq "" || [string index $line 0] eq "#"} {
continue
}
# 解析测试向量,格式: 测试名,输入值,期望输出
set fields [split $line ","]
set test_name [lindex $fields 0]
set input_val [lindex $fields 1]
set expected [lindex $fields 2]
# 运行仿真(这里用模拟代替)
set actual [run_sim_core $input_val]
# 比较结果
if {$actual eq $expected} {
puts $rpt_fp "$test_name: PASS"
incr pass_count
} else {
puts $rpt_fp "$test_name: FAIL (期望 $expected, 实际 $actual)"
incr fail_count
}
}
close $vec_fp
puts $rpt_fp "----------------------------------------"
puts $rpt_fp "总计: [expr {$pass_count + $fail_count}]"
puts $rpt_fp "通过: $pass_count"
puts $rpt_fp "失败: $fail_count"
close $rpt_fp
puts "测试完成,报告已生成: test_report.txt"
3.4 本章知识体系
我把这一章的核心内容画了张图,方便你理解各个知识点之间的关系。
这一章的内容,说白了就是三个字:控、封、读。控制流程让脚本有逻辑,过程封装让代码可复用,文件操作让数据能流通。这三样东西组合起来,你就能写出真正能用的仿真脚本了。
我个人觉得,学Tcl最忌讳的就是“背语法”。语法是死的,场景是活的。你想想看,你写脚本是为了解决仿真中的实际问题,不是为了考试。所以,多想想“这个场景用哪个结构最合适”,比死记硬背重要得多。
好,这一章就到这里。代码示例我都跑过,放心用。遇到问题,多看看错误信息,Tcl的错误提示其实挺友好的。
可可精品资料库