Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,12 +793,27 @@ BenchPod at 192.168.1.220:8080

Start DAC waveform output (network or embeddedci.com).

Give the waveform in volts with `--path`, `--amplitude-v` and `--offset-v` (or in mA on the 4-20 mA
output with `--amplitude-ma` and `--offset-ma`): firmware 3.9.1 and later calibrates it and plays it
at the DAC's full 16 bits, about 50 µV a step on the 3.3 V output. The CLI checks the pod has it
first and refuses on older firmware. `--amplitude` and `--offset` are the older 8-bit levels: 256
steps over the output, no calibration.

```bash
benchpod generate --waveform sine --freq 1kHz --path 3v3 --amplitude-v 1.48 --offset-v 1.65
```

| Flag | Default | Purpose |
|---------------------|---------|-----------------------------------------------------------|
| `--waveform` | (required) | `sine`, `square` or `sawtooth`. |
| `--freq` | `1kHz` | Output frequency with its unit, e.g. `2kHz`; a bare number is Hz. |
| `--amplitude` | `127` | Half-scale amplitude code, 0-127. |
| `--offset` | `128` | DC offset code, 0-255. |
| `--path` | | DAC output for the volts: `3v3`, `5v` or `12v` (firmware 3.9.1+). |
| `--amplitude-v` | | Peak in volts on `--path`, 16-bit and calibrated (firmware 3.9.1+). |
| `--offset-v` | | Centre in volts on `--path`, with `--amplitude-v`. |
| `--amplitude-ma` | | Peak in mA on the 4-20 mA output (J9), 16-bit (firmware 3.9.1+). |
| `--offset-ma` | | Centre in mA on the 4-20 mA output, with `--amplitude-ma`. |
| `--amplitude` | `127` | Half-scale amplitude in 8-bit levels, 0-127. |
| `--offset` | `128` | DC offset in 8-bit levels, 0-255. |
| `--duration` | `0` | How long to play, e.g. `2s` or `500ms`; `0` runs until the next command. |
| `--duration-ms` | `0` | The same in ms (older form of `--duration`). |
| `--sample-rate` | auto | FPGA sample-clock rate, e.g. `10MHz`; leave it out to auto-pick. |
Expand Down
76 changes: 73 additions & 3 deletions cmd/benchpod-cli/commands_tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package main
import (
"encoding/json"
"fmt"
"math"
"os"
"strings"
"time"

"github.com/embeddedci-com/benchpod-cli/internal/spiflash"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -53,9 +55,10 @@ func newPingCmd(g *globalFlags) *cobra.Command {
// ── generate ─────────────────────────────────────────────────────────────────

func newGenerateCmd(g *globalFlags) *cobra.Command {
var waveform string
var waveform, path string
var freq, sampleRate, sampleRateHz float64
var amplitude, offset, durationMS int
var amplitudeV, offsetV, amplitudeMA, offsetMA float64
var duration time.Duration
cmd := &cobra.Command{
Use: "generate",
Expand All @@ -73,6 +76,10 @@ func newGenerateCmd(g *globalFlags) *cobra.Command {
if err != nil {
return err
}
calibrated, err := generateCalibrated(cmd, path, amplitudeV, offsetV, amplitudeMA, offsetMA)
if err != nil {
return err
}
ctx, cancel, pod, err := g.podClient("generate", 30*time.Second)
if err != nil {
return err
Expand All @@ -87,6 +94,27 @@ func newGenerateCmd(g *globalFlags) *cobra.Command {
"offset": offset,
"duration_ms": durationMS,
}
if calibrated != nil {
// Firmware before 3.9.1 ignores the volts and would play its default
// full-scale 8-bit waveform instead: check before sending.
status, err := pod.client.Command(ctx, map[string]any{"cmd": "status"})
if err != nil {
return fmt.Errorf("generate: status: %w", err)
}
ok, err := spiflash.HasCap(status, "generate16")
if err != nil {
return fmt.Errorf("generate: %w", err)
}
if !ok {
return fmt.Errorf("generate: --amplitude-v/--amplitude-ma need firmware 3.9.1 or newer " +
"(the pod does not list generate16); update it with `benchpod flash-self`, or use --amplitude/--offset levels")
}
delete(req, "amplitude")
delete(req, "offset")
for k, v := range calibrated {
req[k] = v
}
}
if sampleRate > 0 {
req["sample_rate_mhz"] = sampleRate
}
Expand All @@ -109,15 +137,57 @@ func newGenerateCmd(g *globalFlags) *cobra.Command {
}
cmd.Flags().StringVar(&waveform, "waveform", "", "waveform: sine|square|sawtooth (required)")
cmd.Flags().Var(newFrequencyValue(1000, &freq), "freq", "output frequency, e.g. 1kHz (a bare number is Hz)")
cmd.Flags().IntVar(&amplitude, "amplitude", 127, "half-scale amplitude 0-127")
cmd.Flags().IntVar(&offset, "offset", 128, "DC offset 0-255")
cmd.Flags().IntVar(&amplitude, "amplitude", 127, "half-scale amplitude in 8-bit levels 0-127")
cmd.Flags().IntVar(&offset, "offset", 128, "DC offset in 8-bit levels 0-255")
cmd.Flags().StringVar(&path, "path", "", "DAC output for --amplitude-v/--offset-v: 3v3|5v|12v (firmware 3.9.1+)")
cmd.Flags().Float64Var(&amplitudeV, "amplitude-v", 0, "peak in volts on --path, played calibrated at 16 bits (firmware 3.9.1+)")
cmd.Flags().Float64Var(&offsetV, "offset-v", 0, "centre in volts on --path (with --amplitude-v)")
cmd.Flags().Float64Var(&amplitudeMA, "amplitude-ma", 0, "peak in mA on the 4-20 mA output, 16-bit (firmware 3.9.1+)")
cmd.Flags().Float64Var(&offsetMA, "offset-ma", 0, "centre in mA on the 4-20 mA output (with --amplitude-ma)")
cmd.Flags().DurationVar(&duration, "duration", 0, "how long to output, e.g. 2s or 500ms (0 = run until next command)")
cmd.Flags().IntVar(&durationMS, "duration-ms", 0, "duration in ms (0 = run until next command); same as --duration")
cmd.Flags().Var(newFrequencyValue(0, &sampleRateHz), "sample-rate", "FPGA sample-clock rate, e.g. 10MHz (omit to auto-pick)")
cmd.Flags().Float64Var(&sampleRate, "sample-rate-mhz", 0, "FPGA sample-clock rate in MHz (omit to auto-pick); same as --sample-rate")
return cmd
}

// generateCalibrated turns the volts / milliamp flags into the calibrated generate fields
// (firmware 3.9.1+), or nil when none of them is set. The 8-bit --amplitude/--offset cannot be
// mixed in.
func generateCalibrated(cmd *cobra.Command, path string, ampV, offV, ampMA, offMA float64) (map[string]any, error) {
f := cmd.Flags()
volts := f.Changed("amplitude-v") || f.Changed("offset-v") || f.Changed("path")
amps := f.Changed("amplitude-ma") || f.Changed("offset-ma")
if !volts && !amps {
return nil, nil
}
if f.Changed("amplitude") || f.Changed("offset") {
return nil, fmt.Errorf("--amplitude/--offset are 8-bit levels: use them or the volts/mA flags, not both")
}
if volts && amps {
return nil, fmt.Errorf("give volts (--path, --amplitude-v, --offset-v) or mA (--amplitude-ma, --offset-ma), not both")
}
if amps {
if !f.Changed("amplitude-ma") || !f.Changed("offset-ma") {
return nil, fmt.Errorf("--amplitude-ma and --offset-ma go together")
}
if !(ampMA > 0) {
return nil, fmt.Errorf("--amplitude-ma must be above 0")
}
return map[string]any{"amplitude_ua": math.Round(ampMA * 1000), "offset_ua": math.Round(offMA * 1000)}, nil
}
if path != "3v3" && path != "5v" && path != "12v" {
return nil, fmt.Errorf("--path must be 3v3, 5v or 12v with --amplitude-v")
}
if !f.Changed("amplitude-v") || !f.Changed("offset-v") {
return nil, fmt.Errorf("--amplitude-v and --offset-v go together with --path")
}
if !(ampV > 0) {
return nil, fmt.Errorf("--amplitude-v must be above 0")
}
return map[string]any{"path": path, "amplitude_v": ampV, "offset_v": offV}, nil
}

// ── capture / stream ─────────────────────────────────────────────────────────

func newCaptureCmd(g *globalFlags) *cobra.Command {
Expand Down
81 changes: 81 additions & 0 deletions cmd/benchpod-cli/generate16_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package main

import (
"encoding/json"
"testing"
)

// generate --amplitude-v/--amplitude-ma: the calibrated 16-bit form (firmware 3.9.1+). The CLI
// asks the pod for its caps first, because older firmware would ignore the volts and play its
// default full-scale 8-bit waveform.
func TestGenerateCalibrated(t *testing.T) {
t.Setenv("BENCHPOD_CONNECTION", "")
t.Setenv("BENCHPOD_TIMEOUT", "")
addr, got := fakeLANPod(t, `{"status":"ok","data":{"caps":["dac","generate16"],"bits":16}}`)
sent := func() map[string]any {
t.Helper()
var req map[string]any
if err := json.Unmarshal([]byte(<-got), &req); err != nil {
t.Fatal(err)
}
return req
}

if code := run([]string{"generate", "--waveform", "sine", "--freq", "1kHz", "--path", "3v3",
"--amplitude-v", "1.48", "--offset-v", "1.65", "--connection", addr}); code != exitOK {
t.Fatalf("volts: exit %d", code)
}
if st := sent(); st["cmd"] != "status" {
t.Fatalf("first request = %v, want status", st)
}
req := sent()
if req["path"] != "3v3" || req["amplitude_v"] != 1.48 || req["offset_v"] != 1.65 {
t.Fatalf("volts request = %v", req)
}
if _, ok := req["amplitude"]; ok {
t.Fatalf("8-bit amplitude sent with volts: %v", req)
}

if code := run([]string{"generate", "--waveform", "square", "--amplitude-ma", "4", "--offset-ma", "12",
"--connection", addr}); code != exitOK {
t.Fatalf("mA: exit %d", code)
}
sent()
if req := sent(); req["amplitude_ua"] != 4000.0 || req["offset_ua"] != 12000.0 || req["path"] != nil {
t.Fatalf("mA request = %v", req)
}
}

func TestGenerateCalibratedNeedsFirmware391(t *testing.T) {
t.Setenv("BENCHPOD_CONNECTION", "")
t.Setenv("BENCHPOD_TIMEOUT", "")
addr, got := fakeLANPod(t, `{"status":"ok","data":{"caps":["dac"]}}`)
if code := run([]string{"generate", "--waveform", "sine", "--path", "5v", "--amplitude-v", "0.1",
"--offset-v", "2.5", "--connection", addr}); code == exitOK {
t.Fatal("generate with volts accepted on a pod without generate16")
}
if st := <-got; st != `{"cmd":"status"}` {
t.Fatalf("request = %s", st)
}
select {
case req := <-got:
t.Fatalf("sent %s to a pod without generate16", req)
default:
}
}

func TestGenerateCalibratedFlagErrors(t *testing.T) {
t.Setenv("BENCHPOD_CONNECTION", "")
for _, args := range [][]string{
{"--path", "3v3", "--amplitude-v", "1"}, // no offset
{"--path", "ext", "--amplitude-v", "1", "--offset-v", "1"}, // bad path
{"--amplitude-v", "1", "--offset-v", "1"}, // no path
{"--path", "3v3", "--amplitude-v", "1", "--offset-v", "1", "--amplitude", "5"}, // mixed with levels
{"--path", "3v3", "--amplitude-v", "1", "--offset-v", "1", "--amplitude-ma", "4", "--offset-ma", "12"},
{"--amplitude-ma", "0", "--offset-ma", "12"},
} {
if code := run(append([]string{"generate", "--waveform", "sine", "--connection", "127.0.0.1:1"}, args...)); code == exitOK {
t.Errorf("%v accepted", args)
}
}
}
Loading