#!/usr/bin/env python3 """ Generate a C-style PWM lookup table. Defaults: - max PWM: 0xD480 (54400) - period: 10 ms - spacing: 20 us -> 500 samples - mode: 'half' (one half-wave over the period) - thresholds and zero-boundary configurable - emits uint16_t array Examples: python LUT.py --mode half --period-us 10000 --spacing-us 20 --array-name pwm_sine --per-line 10 python LUT.py --mode full --hex """ import argparse from math import sin, pi def positive_int(v): iv = int(v, 0) if iv <= 0: raise argparse.ArgumentTypeError("value must be > 0") return iv def nonneg_int(v): iv = int(v, 0) if iv < 0: raise argparse.ArgumentTypeError("value must be >= 0") return iv def build_table(max_pwm, period_us, spacing_us, upper, lower, zero_boundary, amplitude, phase_deg, samples, mode): # Determine sample count if samples is None: if period_us % spacing_us != 0: raise ValueError( "period_us must be a multiple of spacing_us, or pass --samples.") n = period_us // spacing_us else: n = int(samples) if n <= 1: raise ValueError("samples must be >= 2") # Normalize thresholds if upper is None: upper = max_pwm upper = min(max_pwm, max(0, int(upper))) lower = min(max_pwm, max(0, int(lower))) if lower > upper: raise ValueError("lower must be <= upper") if amplitude <= 0 or amplitude > 1.0: raise ValueError("amplitude must be in (0, 1]") if zero_boundary < 0: raise ValueError("zero_boundary must be >= 0") # Precompute phase_rad = phase_deg * pi / 180.0 vals = [] if mode == "half": # One half-wave across the whole table: theta in [0 .. π] # Use (n-1) in denominator so first=0 and last=0 exactly. denom = max(1, n - 1) for k in range(n): theta = pi * (k / denom) + phase_rad s = sin(theta) if s < 0: s = 0.0 # half-wave rectified raw = int(round(s * amplitude * max_pwm)) clipped = max(lower, min(upper, raw)) if clipped < zero_boundary: clipped = 0 vals.append(clipped) elif mode == "full": # Full unipolar: theta in [0 .. 2π), offset to [0..1] for k in range(n): theta = 2.0 * pi * (k / n) + phase_rad s = (sin(theta) * amplitude + 1.0) * 0.5 raw = int(round(s * max_pwm)) clipped = max(lower, min(upper, raw)) if clipped < zero_boundary: clipped = 0 vals.append(clipped) else: raise ValueError("mode must be 'half' or 'full'") return vals def format_c_array(values, array_name, c_type, radix_hex, per_line, add_header, comment): n = len(values) lines = [] if comment: lines.append("/* " + comment + " */") if add_header: lines.append("#include ") lines.append("const {ctype} {name}[{n}] = {{".format(ctype=c_type, name=array_name, n=n)) def fmt(v): if radix_hex: return "0x{0:04X}".format(v & 0xFFFF) return str(v) for i in range(0, n, per_line): chunk = values[i:i+per_line] trailing_comma = "," if (i + per_line) < n else "" lines.append(" " + ", ".join(fmt(v) for v in chunk) + trailing_comma) lines.append("};") return "\n".join(lines) def main(): p = argparse.ArgumentParser(description="Generate a C-style PWM lookup table.") p.add_argument("--max-pwm", type=positive_int, default=0xD480, help="Max PWM (default 0xD480=54400)") p.add_argument("--period-us", type=positive_int, default=10_000, help="Period in us (default 10000 = 10 ms)") p.add_argument("--spacing-us", type=positive_int, default=20, help="Spacing in us (default 20)") p.add_argument("--samples", type=positive_int, default=None, help="Override sample count") p.add_argument("--mode", choices=["half", "full"], default="half", help="Wave mode: 'half' = one half-wave (default), 'full' = full unipolar cycle") p.add_argument("--upper", type=nonneg_int, default=None, help="Upper clamp (default: max-pwm)") p.add_argument("--lower", type=nonneg_int, default=0, help="Lower clamp (default: 0)") p.add_argument("--zero-boundary", type=nonneg_int, default=1000, help="Values below this set to 0 after clipping") p.add_argument("--amplitude", type=float, default=0.5, help="Amplitude scale in (0,1] (default 1.0)") p.add_argument("--phase-deg", type=float, default=0.0, help="Phase offset degrees (default 0)") p.add_argument("--array-name", default="pwm_sine", help="C array name (default pwm_sine)") p.add_argument("--c-type", default="uint16_t", help="C integer type (default uint16_t)") p.add_argument("--hex", action="store_true", help="Emit hex values") p.add_argument("--per-line", type=positive_int, default=10, help="Values per line (default 10)") p.add_argument("--no-header", action="store_true", help="Do not emit #include line") p.add_argument("--comment", default=None, help="Optional leading block comment") args = p.parse_args() vals = build_table( max_pwm=args.max_pwm, period_us=args.period_us, spacing_us=args.spacing_us, upper=args.upper, lower=args.lower, zero_boundary=args.zero_boundary, amplitude=args.amplitude, phase_deg=args.phase_deg, samples=args.samples, mode=args.mode ) if args.comment is None: args.comment = ("Auto-generated PWM table: mode={m}, max_pwm={mp} (0x{mp:X}), " "period_us={per}, spacing_us={sp}, samples={n}, " "upper={up}, lower={lo}, zero_boundary={zb}, " "amplitude={a}, phase_deg={ph}".format( m=args.mode, mp=args.max_pwm, per=args.period_us, sp=args.spacing_us, n=len(vals), up=(args.upper if args.upper is not None else args.max_pwm), lo=args.lower, zb=args.zero_boundary, a=args.amplitude, ph=args.phase_deg)) out = format_c_array( values=vals, array_name=args.array_name, c_type=args.c_type, radix_hex=args.hex, per_line=args.per_line, add_header=not args.no_header, comment=args.comment ) print(out) if __name__ == "__main__": main()