180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
#!/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 <stdint.h>")
|
|
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=544, help="Values below this set to 0 after clipping")
|
|
p.add_argument("--amplitude", type=float, default=0.8, 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()
|