Logic level parsing Rigol oscilloscope CSV data with F#

Posted on January 12, 2015

I was recently debugging a SPI communication but had no logic analyser. Fortunately my oscilloscope (rigol DS1052e) can export a capture as CSV (I assume that most digital oscilloscopes can export to CSV too).
Parsing the CSV is trivial, getting a logic level from it is also fairly easy. I realise that this is terribly quick & dirty but it worked 100% for what I needed. Here is the high level process

  1. Ignore timings - SPI is clocked so actual timing is irrelevant for this analysis
  2. Parse X and Y channels
  3. Convert clock channel voltage into logic level 1 or 0
  4. Discard all rows apart from rows where the clock goes high
  5. Convert data channel to logic level
  6. Group into bytes
  7. Display hex value

As I said, very simple but it worked well and I was able to see exactly what was going on. Obviously this only works for synchronous (clocked) protocols like SPI/I2C but that is all I needed.

Here is the full F# script (also see the gist)

open System
open System.IO
open System.Text
open System.Globalization
open System.Text.RegularExpressions
 
let ttlTrue = 2.3
 
let getLines (fname:string) = seq{
  use stream = new StreamReader( fname )
  while not stream.EndOfStream do
     yield stream.ReadLine()
}
 
let parseVoltage (s:string) =
  try
    System.Double.Parse( (s.Replace( ".", "," )), NumberStyles.Float )
  with
  | e ->
    printfn "%s - %s" s (e.ToString())
    0.0
 
let filterOnClockGoingHigh (s:seq<int * int>) = seq {
  let e = s.GetEnumerator()
  let last = ref 0
 
  while e.MoveNext() do
    if !last <> (snd e.Current) then
      last := (snd e.Current)
      if !last = 1 then
        yield e.Current
}
 
//from http://fssnip.net/6A
let groupWhen f (input:seq<_>) = seq {
   use en = input.GetEnumerator()
   let running = ref true
 
   // Generate a group starting with the current element. Stops generating
   // when it founds element such that 'f en.Current' is 'true'
   let rec group() =
     [ yield en.Current
       if en.MoveNext() then
         if not (f en.Current) then yield! group()
       else running := false ]
 
   if en.MoveNext() then
     // While there are still elements, start a new group
     while running.Value do
       yield group() |> Seq.ofList }
 
let toNum (g:int seq) =
  let l = List.ofSeq g
 
  (if l.Length > 0 && l.[0] = 1 then 128uy else 0uy) +
  (if l.Length > 1 && l.[1] = 1 then  64uy else 0uy) +
  (if l.Length > 2 && l.[2] = 1 then  32uy else 0uy) +
  (if l.Length > 3 && l.[3] = 1 then  16uy else 0uy) +
  (if l.Length > 4 && l.[4] = 1 then   8uy else 0uy) +
  (if l.Length > 5 && l.[5] = 1 then   4uy else 0uy) +
  (if l.Length > 6 && l.[6] = 1 then   2uy else 0uy) +
  (if l.Length > 7 && l.[7] = 1 then   1uy else 0uy)
 
getLines fsi.CommandLineArgs.[1]
//Skip headers
|> Seq.skip 2
//Split CSV
|> Seq.map (fun l -> l.Split( [|','|] ))
//Ignore empty lines
|> Seq.filter (fun f -> f.Length >= 3)
//Parse the voltages
|> Seq.map (fun f -> (parseVoltage f.[1], parseVoltage f.[2]) )
 
//Clock from voltage to logic level
|> Seq.map (fun (data,clock) -> ((if data > ttlTrue then 1 else 0), (if clock > ttlTrue then 1 else 0)))
//Get data - clock rising edge
|> filterOnClockGoingHigh
//remove the clock data
|> Seq.map (fun f -> fst f)
//Add a count to each item
|> Seq.mapi (fun i v -> (i,v))
//group into chunks of 8
|> groupWhen (fun (i,v) -> i % 8 = 0)
//get only the second item
|> Seq.map (Seq.map snd)
//convert each group to a number
|> Seq.map (fun x -> toNum x)
 
|> Seq.iter (fun x -> printfn "%02x %c" x (if (x >= 0x20uy) && (x <= 0x7euy) then char x else ' ') )