Oscilloscope

Project from RJM

2004-08-04 Here a simple oscilloscope functionality is presented. By the time this is stripped for the primary purpose of an investigation (event/response delay investigation). Later, further functionality (e.g. axes) will be (re)added.

The application presumes a serial connection to a device providing sample data. Samples cause canvas update just as they arrive.

oscill.tcl provides a basic oscilloscope functionality. When a trace is drawn, the previous trace is removed sample by sample. A small (moving) gap in the trace shows the actual update location. Command line options:

 1...n - specifies com port for target device delivering measuring data
   0   - oscill.tcl  automatically starts msp430dummy.tcl and creates a
         pipe
       - (no option) in internal proc provides in timing and simulated
         measuring data

datadummy.tcl replaces the original measuring frontend. Command line options:

 1...n - specifies com port for connection with oscill.tcl via a
         nullmodem cable. When the PC provides two com ports, both
         programs can run on the same machine. However, better
         performance had been observed on separate machines during the
         phase of optimizing this experimental software.

Here both scripts are inserted:

 # oscill.tcl
 ################################################################################
 # oscillograph module
 ################################################################################
 
 array set oscpar {
     single 1
     samples 400
     plotstep 2
     timebase 50
     persist 0
     channels 2
     location +1+24
     x0 15
 }
 
 
 # initialize STRIPPED window
 proc oscinit {runcmd} {
     global oscpar
 
     # initialize Tk widgets
     if ![winfo exists .osc] {
         toplevel .osc
         wm title .osc "oscillogram"
         # specify as window not iconizable, '.' is master window
         wm transient .osc .
         wm geometry .osc $oscpar(location)
         wm geometry .osc {}   ;# ensure internal widget size appearing
     
         canvas .osc.c -width 830 -height 550 -bg white
         frame .osc.f1
         button .osc.run -text "<space> run" -command $runcmd
         checkbutton .osc.single -text "1 single" -variable oscpar(single)
         label .osc.tbl -text "2 timebase (ms/div)"
         set oscpar(tbvalues) {10 20 50 100 200 500 1000}
         spinbox .osc.tb -values $oscpar(tbvalues) -width 4 \
                        -command {tb_update %s; .osc.tb selection range 0 end}
         .osc.tb set $oscpar(timebase)
         .osc.tb configure -validate key -vcmd {regexp {^[-+]?[.\d]*$} %P}
         checkbutton .osc.persist -text "9 persistence" -variable oscpar(persist)
         button .osc.clr -text "0 clr" -command {.osc.c delete curve oldcurve}
         pack .osc.run .osc.single .osc.tbl .osc.tb \
                 .osc.persist .osc.clr -side left -fill y -padx 4 -in .osc.f1
         pack .osc.c .osc.f1
         # basic vertical lines replaces axis system
         set x $oscpar(x0)
         for {set n 0} {$n <= 10} {incr n} {
             .osc.c create line $x 0 $x 512 -fill grey
             incr x [expr {$oscpar(samples)/5}]
         }
         
         
         # here define extra osc bindtag for numeric key bindings: active only upon
         # window focus (not active when entry, spinbox etc. focus)
         bindtags .osc "wosc [bindtags .osc]"
  
         bind wosc <Key-1> {.osc.single toggle}
         bind wosc <Key-2> {focus .osc.tb}
         bind wosc <Key-9> {.osc.persist toggle}
         bind wosc <Key-0> {.osc.clr invoke}
         bind .osc.tb <Key-Escape> {focus .osc}
         bind .osc.tb <Key-Return> {tb_update [.osc.tb set]; .osc.tb selection range 0 end}
         bind wosc <Key-Escape> {destroy .osc}
         bind wosc <Key-Return> {focus .osc}
         bind all <Key-space> {.osc.run invoke; break}
         # let these bindings catch space key to only allow other bindings
         bind Spinbox <Key-space> {continue}
         bind Entry <Key-space> {continue}
         # here filter for .osc.tb: no alpha characters (space allowed, but
         # captured in a new class binding)
         bind .osc.tb <KeyPress> {
             if {[regexp -nocase {[A-Z]} %A]} {
                 break   ;# here inhibit further bind processing
             }
         }
         bind wosc <Destroy> {set oscpar(location) [wm geometry .osc]}
         tb_update $oscpar(timebase)
     }
     focus .osc
 }
 
 # argument: a list containing values for each sample, according to the number of channels
 set xpos 0
 proc plotdata {yy} {
     global oscpar y_expr lcolor
     # variables with only static nature
     global objID xpos
     
     # CREATE line first, only starting point
     if {!$xpos} {
         set xpos $oscpar(x0)
         if {$oscpar(persist)} {
             .osc.c itemconfigure curve -fill darkgrey
             .osc.c addtag oldcurve withtag curve
             .osc.c dtag curve
         } else {
             .osc.c addtag lastcurve withtag curve
             .osc.c dtag curve
             .osc.c dchars lastcurve 1 6  ;# create gap in dynamic curve plotting
         }
         catch {unset objID}
         set index 0
         foreach y $yy color $lcolor ex $y_expr {
             set y [expr $ex]
             if {$color=="-"} {
                 lappend objID 0
             } else {
                 lappend objID [.osc.c create line $xpos $y $xpos $y \
                     -fill $color -tags "object curve curve($index)"]
             }
         incr index
         }
     } else {
         # +++++++++++++ the CORE of the line drawing functionality ++++++++++++++
         # delete first coordinate pair of last curve(s)
         .osc.c dchar lastcurve 1  ;#  !!! consumes time (check if foraeach & ID would be better than tag)
         # append additional line pieces to one line object
         foreach y $yy ex $y_expr id $objID {
             set y [expr $ex]
             #set y [expr 400-$y/8.0]
             .osc.c insert $id end "$xpos $y"
         }
     }
     incr xpos ${oscpar(plotstep)}
     if {$xpos == [expr {$oscpar(samples)*$oscpar(plotstep)+$oscpar(x0)}]} {
         set xpos 0
         if {!$oscpar(single) } {
             after idle .osc.run invoke
         }
     }
 }
 
 proc tb_update {s} {
     global oscpar serial
 
     set oscpar(timebase) $s
     .osc.tb config -values $oscpar(tbvalues)
     .osc.tb set $oscpar(timebase)
     # prepare sample rate
     set interval [expr {$s*1000 / ($oscpar(samples)/10)}]
     if {$interval > 4000} \
         {set interval [expr {$interval/1000}]; set unit m} \
         {set unit u}
     if {[info exists serial]} {
         puts -nonewline $serial "T$unit[binary format S $interval]"
         flush $serial
     }
     # now prepare total scan time representation
     set unit ms
     if {$s >= 1000} {
         set unit s
         set s [expr {$s/1000}]
     }
     .osc.c delete text
     .osc.c create text 650 525 -text "scan time: [expr {$s*10}] $unit" -tags text
     return 1
 }
 
 ####################################
 
 # this every uses a repeats limit and a fixed 1ms inner interval (1 ms is a reliable rate)
 set _count 0
 set _rep 0
 proc every {ms repeats body} {
     global _count _rep
     
     if {$_rep <= 0} {set _rep $repeats}
     if {!$_count} {set _count [expr {$ms ? $ms : 1}]}
     set id [after 1 [info level 0]]   ;# use 1 and count for ms ms
     if {[incr _count -1]} return
     # timed script execute
     if 1 $body
     if {![incr _rep -1]} {after cancel $id}
 }
 
 # this proc is used when an external process (pipe or com port) supplies raw plot data
 proc plotprocess {} {
     global serial oscpar
     
     # send arming command to target device
     puts -nonewline $serial "A[binary format S $oscpar(samples)][binary format c $oscpar(channels)]"
     flush $serial
 }
 
 # this proc is used when this script supplies raw plot data according to timebase setting
 proc plotdummy {} {
     global oscpar tp
     
     set tp(n) 0
     every [expr {$oscpar(timebase)/35}] $oscpar(samples) {
         global tp
         set x $tp(n)
         incr tp(n)
         # simulate analog values in the range 0-1600 (display range 0-4095, 512 pixel)
         plotdata "[expr {4*$x}] [expr {4*(400-$x) + 100*rand()}]"
         #puts -nonewline .
     }
 }
 
 # input data words (n = number of words, returned in string)
 # note: this procedure works correct after incompleted $stream
 # next call through fileevent it will be completed
 set ser_in_cnt 0
 proc ser_in_w {n} {
     global serial stream ser_in_cnt
 
     if {!$ser_in_cnt} {set ser_in_cnt [expr {$n*2}]}
     append stream [read $serial $ser_in_cnt]
     set ser_in_cnt [expr {$n*2-[string length $stream]}]
     if {$ser_in_cnt} return
 
     binary scan $stream S$n intval
     set stream ""
     return $intval
 }
 
 proc getdata {} {
     global oscpar
     
     if {[set yy [ser_in_w $oscpar(channels)]] != ""} {
         plotdata $yy
     }
     # let next fileevent complete the receive message if not yet complete
 }
 
 ###############################################
 
 lappend y_expr {512 - $y/8.0}; lappend lcolor blue
 lappend y_expr {512 - $y/8.0}; lappend lcolor red
 
 if {$argc && [string length $argv] == 1} {
     # initialize port status
     if {!$argv} {
         # when 0, open a pipe: connects to a MSP430 hardware simulator script
         set serial [open "|wish datadummy.tcl" r+]
     } else {
         set serial [open com$argv r+]
         fconfigure $serial -timeout 1000 -handshake none -pollinterval 4
         fconfigure $serial -mode 57600,n,8,1    ;#only necessary to force 8 bit (default is 7)
     }
     fconfigure $serial -buffersize 4096
     # note: blocking 0 must be used in order to allow event loop processing when 
     # waiting for data
     fconfigure $serial -blocking 0 -encoding binary -translation binary
     fileevent $serial readable getdata
     oscinit plotprocess
 } else {
     oscinit plotdummy
 }

The drawing core of this script can be found just under the phrase "CORE".

Here is the data simulator (next filename must be used)

 # datadummy.tcl
 # dummy MSP430 unit. Uses pipes or serial port to simulate MSP430 target device
 # may also dummy remotely per serial port (start with port# as argument)
 #console show
 
 if {$argc} {
     set fid [open com$argv r+]
     fconfigure $fid -timeout 1000 -handshake none
     fconfigure $fid -mode 57600,n,8,1
     set in $fid
     set out $fid
 } else {
     set in stdin
     set out stdout
 }
 fileevent $in readable "getcommands"
 # blocking 0 should result in immediate flush return
 fconfigure $out -blocking 0 -encoding binary -translation binary
 fconfigure $in -blocking 0 -encoding binary -translation binary
 
 pack [text .t]
 array set param {
     time 1
     unit ms
     rampstart 0
     rampstep 4
 }
 
 proc getcommands {} {
     global in out param y sample
     
     .t insert end >
     switch [read $in 1] {
         A {.t insert end "Arm..."
            binary scan [read $in 2] S1 param(samples)    ;# number of samples
            read $in 1    ;# command (1 = stop/quit arming)
            set y $param(rampstart)
            set sample 0
            set ms [expr {$param(unit)=="m" ? $param(time) : $param(time)/1000}]
            .t insert end " ($ms)  $param(samples) samples\n"
            every $ms $param(samples) sample
           }
         I {.t insert end "Channel setup [read $in 2]\n"
           }
         T {.t insert end "Timebase set: [set param(unit) [read $in 1]] "
            binary scan [read $in 2] S1 param(time)
            .t insert end "$param(time)\n"
           }
         R {.t insert end "Ramp set: "
            binary scan [read $in 2] S1 param(rampstart)
            binary scan [read $in 2] S1 param(rampend)
            .t insert end "$param(rampstart)  $param(rampend)\n"
            set param(rampstep) [expr {($param(rampend)-$param(rampstart)) / 400}]
           }
         S {.t insert end "Suspend arming\n"
           }
         Q {.t insert end "Quit arming\n"
           }
         D {.t insert end "Digital Out\n"
            read $in 1
           }
         C {.t insert end "Custom: \n"
            read $in 1}
         B {.t insert end "Firmware Build\n"
            puts $out "000"; flush $out
           }
     }
     .t see insert
 }
 
 set count 0
 set rep 0
 proc every {ms repeats body} {
     global count rep
     
     if {!$rep} {set rep $repeats}
     if {!$count} {set count [expr {$ms ? $ms : 1}]}
     set id [after 1 [info level 0]]   ;# use 1 and count for ms ms
     if {[incr count -1]} return
     # timed script execute
     if 1 $body
     if {![incr rep -1]} {after cancel $id}
 }
 
 
 proc sample {} {
         global out y param sample
 
         set x $sample
         incr sample
         puts -nonewline $out [binary format S2 [list [expr {int($y)}] [expr {int(4*(400-$x)+100*rand())}]]]
         flush $out
         set y [expr {$y + $param(rampstep)}]
 }