![]() |
- used raspi-config to enable I2C and SPI on the SBC (tutorial here)
- SSH'd into the SBC
- cd'd to the directory containing the file
- ran it: python3 [name of the script]
- Outputs were visible at DAC output pins and/or the SBC's via its terminal.
I2C--MCP4728
![]() |
| I2C wiring (SDA, SCL) is the same for other I2C parts used in this post. |
![]() |
| To orient this correctly, put the SBC on the bench and face its HDMI jack to the left. Useful webpage and site describing this is here. |
![]() |
| Pinout of the MCP4728: (p. 22 of data sheet) 1. Vdd: 3.3V 2 and 3 I2C. 4: tie to GND 5: EEPROM (not used for the examples; let it float) 6-9: outputs, 10: tie to GND |
![]() |
| MCP4728 Breakout Board. How to program the IC begins on page 23 of its datasheet. |
import smbus2 as smbus
#smbus2 can be imported from uv--smbus can't
########################
"""
example: writing value(s) to a single channel of the mcp4728 (B in this case)
even at 400K i2c, not a very fast process, so not good for everything.
OK for simple tasks.
"""
bus = smbus.SMBus(1)
DEVICE_ADDRESS = 0x60 #default addr.
def set_channel_b_voltage(value, vref=1, gain=0):
"""
Sets voltage for CHANNEL B only using Multi-Write Command.
value: 0-4095
vref: 1 (Internal 2.048V), 0 (VDD)
gain: 0 (x1), 1 (x2)
"""
value = max(0, min(4095, value))
# 1. Build Command Byte for Channel B
# Command: 0 1 0 0 0 (Multi-Write)
# Channel B: 0 1
# UDAC: 0 (Update now)
# Binary: 01000010 -> Hex: 0x42
command_byte = 0x42
# 2. Build Upper Data Byte (Config + D11-D8)
# Bits: VREF PD1 PD0 Gx D11 D10 D9 D8
# We assume PD1=0, PD0=0 (Normal Power)
upper_data_bits = (value >> 8) & 0x0F
config_bits = (vref << 7) | (0 << 5) | (gain << 4)
byte2 = config_bits | upper_data_bits
# 3. Build Lower Data Byte (D7-D0)
byte3 = value & 0xFF
# 4. Send Command
try:
# write_i2c_block_data(Address, CommandByte, [DataBytes])
bus.write_i2c_block_data(DEVICE_ADDRESS, command_byte, [byte2, byte3])
print(f"Channel B updated to {value}")
except OSError as e:
print(f"Error: {e}")
# --- Test ---
# Set Channel B to ~1.024V (Half Scale)
import smbus2 as smbus
import time
#smbus2 can be imported from uv--smbus can't
########################
"""
example: writing to a all four channels of the mcp4728
even at 400K i2c, not a very fast process, so not good for everything.
but--fast write is faster vs writing data to each channel individually.
"""
# Initialize I2C
bus = smbus.SMBus(1)
DEVICE_ADDRESS = 0x60
def set_all_channels(va, vb, vc, vd):
"""
Updates all 4 channels (A, B, C, D) simultaneously using Fast Write.
Values must be 0-4095.
"""
# 1. Clamp values to 12-bit range (0-4095)
values = [va, vb, vc, vd]
data_payload = []
for val in values:
val = max(0, min(4095, val))
# 2. Split 12-bit value into two bytes
# Byte 1: 0 0 0 0 D11 D10 D9 D8 (Assuming Normal Power Mode)
# We shift the value right by 8 bits to get the top nibble
# and mask with 0x0F to ensure the command bits (top 4) are 0000.
upper_byte = (val >> 8) & 0x0F
# Byte 2: D7 D6 D5 D4 D3 D2 D1 D0
lower_byte = val & 0xFF
data_payload.extend([upper_byte, lower_byte])
# 3. Send Data
# write_i2c_block_data(Address, Register/Cmd, [Data List])
# The MCP4728 Fast Write command doesn't use a standard register address.
# The "Command" IS the first byte of the data stream.
# We strip the first byte off our payload to use as the "Cmd" argument,
# and pass the rest as the list of data bytes.
try:
bus.write_i2c_block_data(DEVICE_ADDRESS, data_payload[0], data_payload[1:])
except OSError as e:
print(f"I2C Error: {e}")
# --- Example Usage ---
try:
print("Starting 4-Channel Waveform...")
while True:
# Example: Set different static voltages
# A=0V, B=1V, C=2V, D=Full Scale (approx)
set_all_channels(0, 2000, 3000, 4095)
time.sleep(1)
# Example: Invert them
set_all_channels(4095, 3000, 2000, 0)
time.sleep(1)
except KeyboardInterrupt:
# Turn off outputs on exit
set_all_channels(0, 0, 0, 0)
print("\nStopped.")
import smbus2 as smbus
import time
"""
needs smbus2 module--add that first.
uses a lookup table to create_triangle_table
a triangle output on Chan A
Requires 400K I2C
"""
# --- Configuration ---
DEVICE_ADDRESS = 0x60
BUS_NUMBER = 1
DAC_RESOLUTION = 4096
# Multi-Write Command Constants
CMD_MULTI_WRITE = 0x40 # 0100 0000
# Channel Select bits (shifted to match DAC1, DAC0 position)
CH_A = 0x00 # 0000 0000 (Channel A)
CH_B = 0x02 # 0000 0010 (Channel B)
# --- I2C Setup ---
try:
bus = smbus.SMBus(BUS_NUMBER)
except Exception as e:
print(f"Error opening I2C bus: {e}")
exit(1)
def create_triangle_table(steps=1000):
"""
Creates a full cycle triangle wave lookup table (0 -> 4095 -> 0).
"""
table = []
# Rising edge
for i in range(steps // 2):
val = int((i / (steps // 2)) * (DAC_RESOLUTION - 1))
table.append(val)
# Falling edge
for i in range(steps // 2):
val = int((1 - (i / (steps // 2))) * (DAC_RESOLUTION - 1))
table.append(val)
return table
def send_value_channel_a(value):
"""
Sends a specific 12-bit value to CHANNEL A using Multi-Write.
"""
# Clamp value
value = max(0, min(4095, int(value)))
# --- Construct Multi-Write Command ---
# Byte 1: Command (01000) | Channel (00 for A) | UDAC (0)
# Binary: 0100 0000 -> Hex: 0x40
cmd_byte = CMD_MULTI_WRITE | CH_A
# Byte 2: Vref(1) | PD(00) | Gain(0) | Upper Data (D11-D8)
# Vref=1 (Internal 2.048V), Gain=0 (x1)
# 0x80 is the bitmask for Vref=1
upper_nibble = (value >> 8) & 0x0F
byte2 = 0x80 | upper_nibble
# Byte 3: Lower Data (D7-D0)
byte3 = value & 0xFF
try:
bus.write_i2c_block_data(DEVICE_ADDRESS, cmd_byte, [byte2, byte3])
except OSError:
pass
def estimate_max_update_rate(samples=500):
"""
Benchmarks I2C speed on Channel A.
"""
print("Calibrating I2C speed...")
start = time.time()
for _ in range(samples):
send_value_channel_a(2048)
duration = time.time() - start
rate = samples / duration
print(f"Max Update Rate: {rate:.1f} Hz")
return rate
def run_triangle_wave(frequency):
"""
Generates a triangle wave on Channel A at the specified frequency.
"""
TABLE_SIZE = 2048
wave_table = create_triangle_table(TABLE_SIZE)
max_rate = estimate_max_update_rate()
# Calculate Step Size
step_size = (TABLE_SIZE * frequency) / max_rate
print(f"\n--- Outputting {frequency} Hz Triangle Wave on Channel A ---")
print(f"Step Size: {step_size:.4f}")
print("Press CTRL+C to stop.")
current_index = 0.0
try:
while True:
idx = int(current_index) % TABLE_SIZE
val = wave_table[idx]
send_value_channel_a(val)
current_index += step_size
except KeyboardInterrupt:
print("\nStopping...")
send_value_channel_a(0) # Turn off
if __name__ == "__main__":
try:
freq_input = float(input("Enter desired frequency (Hz): "))
run_triangle_wave(freq_input)
except ValueError:
print("Please enter a valid number.")
![]() |
| At 100hz, stair-step but usable (maybe). At higher frequencies the DAC's output became heavily distorted and unusable.... |
sudo vi /boot/firmware/config.txt
#edit these lines:
#dtparam=i2c_arm=on
#dtparam=i2c_baudrate=400000
import smbus2 as smbus
import time
#uv can only import smbus2.
"""
create a triangle lookup table and send 4 signals 90 degrees out of phase
to each of mcp4728's 4 channels.
Works, but output is stairstepped--even at 400k i2c we are pushing
the limits of what this tech can do.
Looks better at sub-audio frequences (eg, as a quadrature LFO)
Prob needs an RC HP filter for each channel....
"""
# --- Configuration ---
DEVICE_ADDRESS = 0x60
BUS_NUMBER = 1
TABLE_SIZE = 2048
DAC_MAX = 4095
# --- I2C Setup ---
try:
bus = smbus.SMBus(BUS_NUMBER)
except Exception as e:
print(f"Error opening I2C bus: {e}")
exit(1)
def create_triangle_table(steps):
"""Generates a lookup table for a 0-4095-0 triangle wave."""
table = []
half_steps = steps // 2
# Rising (0 -> 4095)
for i in range(half_steps):
val = int((i / half_steps) * DAC_MAX)
table.append(val)
# Falling (4095 -> 0)
for i in range(half_steps):
val = int((1 - (i / half_steps)) * DAC_MAX)
table.append(val)
return table
def write_channel(channel_idx, value):
"""
Writes to a SINGLE channel using the Multi-Write Command.
channel_idx: 0=A, 1=B, 2=C, 3=D
"""
value = max(0, min(4095, int(value)))
# --- Command Byte Construction ---
# Bits: 0 1 0 0 0 DAC1 DAC0 UDAC
# Multi-Write Code: 01000 (0x08 << 3 = 0x40)
# Channel: Shifted left by 1 bit
# UDAC: 0 (Update Immediately)
cmd_byte = 0x40 | (channel_idx << 1)
# --- Data Bytes ---
# Byte 2: Vref(1) | PD(00) | Gain(0) | Upper Data
# Vref=1 (Internal 2.048V) -> 0x80
upper_nibble = (value >> 8) & 0x0F
byte2 = 0x80 | upper_nibble
# Byte 3: Lower Data
byte3 = value & 0xFF
try:
bus.write_i2c_block_data(DEVICE_ADDRESS, cmd_byte, [byte2, byte3])
except OSError:
pass
def run_quad_phase_wave(frequency):
wave_table = create_triangle_table(TABLE_SIZE)
# Calculate Phase Offsets (indices)
offset_0 = 0
offset_90 = TABLE_SIZE // 4 # 512
offset_180 = TABLE_SIZE // 2 # 1024
offset_270 = (TABLE_SIZE * 3) // 4 # 1536
# Estimate speed for loop timing
# Since we do 4 separate writes now, it will be slower.
# Let's assume approx 300-400 full updates/sec without sleep.
estimated_updates_per_sec = 350.0
step_size = (TABLE_SIZE * frequency) / estimated_updates_per_sec
print(f"\n--- Outputting {frequency} Hz Quad-Phase Waves ---")
print("Sequential Mode: Updating A -> B -> C -> D")
print("Verify voltages with Multimeter/Scope.")
current_index = 0.0
try:
while True:
# Base Index
base_idx = int(current_index)
# Calculate values for all 4 channels
val_a = wave_table[(base_idx + offset_0) % TABLE_SIZE]
val_b = wave_table[(base_idx + offset_90) % TABLE_SIZE]
val_c = wave_table[(base_idx + offset_180) % TABLE_SIZE]
val_d = wave_table[(base_idx + offset_270) % TABLE_SIZE]
# Send them sequentially
# 0=A, 1=B, 2=C, 3=D
write_channel(0, val_a)
write_channel(1, val_b)
write_channel(2, val_c)
write_channel(3, val_d)
current_index += step_size
except KeyboardInterrupt:
print("\nStopping...")
for i in range(4):
write_channel(i, 0)
if __name__ == "__main__":
try:
freq = float(input("Enter frequency (Hz) [Try 1.0 for multimeter testing]: "))
run_quad_phase_wave(freq)
except ValueError:
print("Invalid number.")
I2C--PCF8591
![]() |
| PCF8591 Breakout Boards are everywhere....and was the only I2C ADC I could find in my junk box. |
PCF8591 as ADC:
import smbus2 as smbus
import time
#reads input 3 of the PCF8591 and displays every 500ms
#on most bob's that's the built in pot.
#make sure to have BOB jumper P6 in place.
bus = smbus.SMBus(1)
ADDRESS = 0x48
# We use 0x43 instead of 0x03
# 0x40 (Enable DAC Output) + 0x03 (Channel 3)
# Many modules require the DAC to be enabled to clock the ADC correctly.
COMMAND = 0x43
def read_ain3_reliable():
# 1. Send Control Byte
bus.write_byte(ADDRESS, COMMAND)
# 2. Dummy Read (Clears the buffer)
bus.read_byte(ADDRESS)
# 3. Real Read (Gets fresh data)
return bus.read_byte(ADDRESS)
print("Reading Potentiometer on AIN3 (Reliable Mode)...")
print("Press CTRL+C to stop.")
try:
while True:
pot_val = read_ain3_reliable()
# Convert to Voltage
voltage = (pot_val / 255.0) * 3.3
# simple bar chart
bar = '#' * int(pot_val / 5)
print(f"Value: {pot_val:<3} | Voltage: {voltage:.2f}V | {bar}", end='\r')
time.sleep(0.1)
except KeyboardInterrupt:
print("\nStopped.")
import smbus2 as smbus
import time
#make sure to uv add smbus2
#reads voltages present on the 4 PCV8591 inputs.
bus = smbus.SMBus(1)
ADDRESS = 0x48
def read_all_channels():
results = []
# We cycle through channels 0 to 3
for channel in range(4):
# 1. Write Control Byte to select the channel
# Control Byte: 0x40 (Enable Output) | channel_index
bus.write_byte(ADDRESS, 0x40 | channel)
# 2. Dummy Read (clears the previous conversion from the buffer)
bus.read_byte(ADDRESS)
# 3. Real Read (gets the current value)
value = bus.read_byte(ADDRESS)
results.append(value)
return results
print("PCF8591 input Scanner...")
print("Change input voltages and see which column changes!")
print("AIN0 AIN1 AIN2 AIN3")
print("---------------------------")
while True:
vals = read_all_channels()
# Print formatted columns
print(f"{vals[0]:<7} {vals[1]:<7} {vals[2]:<7} {vals[3]:<7}", end='\r')
time.sleep(0.2)
PCF8591 as DAC:
import smbus2 as smbus
import time
# --- Configuration ---
DEVICE_ADDRESS = 0x48
BUS_NUMBER = 1
# Control Byte: 0x40
# Bit 6 (1) = Analog Output Enable (Turn on DAC)
# Bits 1-0 (00) = Channel 0 (doesn't matter for DAC, but good default)
CMD_ENABLE_DAC = 0x40
try:
bus = smbus.SMBus(BUS_NUMBER)
except Exception as e:
print(f"Error opening I2C bus: {e}")
exit(1)
def set_dac_value(value):
"""
Writes a value (0-255) to the PCF8591 DAC.
Protocol: [Address] [Control Byte] [Data Byte]
"""
# Clamp value to 8-bit range
value = int(max(0, min(255, value)))
try:
# write_byte_data sends: Address -> Register(Cmd) -> Value
bus.write_byte_data(DEVICE_ADDRESS, CMD_ENABLE_DAC, value)
except OSError:
pass # Ignore I2C errors to keep the wave running
def estimate_speed(samples=500):
"""
Benchmarks how fast your Pi can talk to this specific chip.
"""
print("Calibrating I2C speed...")
start = time.time()
for _ in range(samples):
set_dac_value(128)
duration = time.time() - start
rate = samples / duration
print(f"Max Update Rate: {rate:.1f} Hz")
return rate
def run_ramp_wave(target_freq):
# 1. Benchmark
max_rate = estimate_speed()
# 2. Calculate Step Size
# Total range is 256 steps (0 to 255)
# Total steps needed per second = 256 * frequency
# We can only do 'max_rate' updates per second.
step_size = (256 * target_freq) / max_rate
print(f"\n--- Outputting {target_freq} Hz Ramp Wave ---")
print(f"Step Increment: {step_size:.4f}")
print("Press CTRL+C to stop.")
current_val = 0.0
try:
while True:
# Send current integer value
set_dac_value(current_val)
# Increment
current_val += step_size
# Reset if we hit the top (Sawtooth shape)
if current_val >= 256:
current_val -= 256
except KeyboardInterrupt:
print("\nStopping...")
set_dac_value(0) # Turn off output
if __name__ == "__main__":
run_ramp_wave(10) # 10 Hz Target
I2C OLED
import time
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
# Initialize I2C connection (Port 1 is standard for Pi)
serial = i2c(port=1, address=0x3C)
# Initialize the SSD1306 device
# Note: Ensure width/height match your specific display (usually 128x64 or 128x32)
device = ssd1306(serial, width=128, height=64)
print("Display initialized. Press Ctrl+C to exit.")
try:
while True:
# The 'canvas' allows you to draw on the screen
# It automatically clears the screen at the start of the block
# and displays the result at the end of the block.
with canvas(device) as draw:
# Draw a white rectangle border
draw.rectangle(device.bounding_box, outline="white", fill="black")
# Draw text
draw.text((30, 25), "Hello DiWHY ", fill="white")
# Prevent the script from consuming 100% CPU
time.sleep(1)
except KeyboardInterrupt:
# Clear screen on exit
device.cleanup()
print("Exiting...")
![]() |
| "seems working" |
SPI--MCP3002 ADC
![]() |
| I hate breadboarding.... |
import spidev
import time
# SPI setup
spi = spidev.SpiDev()
spi.open(0, 0) # Bus 0, Device 0 (CE0)
spi.max_speed_hz = 1000000 # 1 MHz
def read_mcp3002(channel):
"""
Reads data from MCP3002 (10-bit ADC).
Channel must be 0 or 1.
"""
if channel > 1 or channel < 0:
return -1
# datasheet Table 5-1 and Fig 5-1 [cite: 1706, 1707]
# We construct the control byte:
# Start Bit (1) | SGL/DIFF (1) | ODD/SIGN (channel) | MSBF (1)
# We position the Start bit to align with the SPI clocking.
# Sending 0x60 (0110 0000) places the start bit at bit 6.
# The (channel << 4) selects CH0 (0) or CH1 (1).
config_bits = 0x60 | (channel << 4)
# We send [Config, Dummy Byte]
# The device returns 10 bits of data.
resp = spi.xfer2([config_bits, 0x00])
# The result usually comes back across the two bytes.
# We need to mask and shift to get the 10-bit value.
# Based on alignment of 0x60, the valid data is in the last 10 bits.
# Parse the 10-bit result
result = ((resp[0] & 0x03) << 8) | resp[1]
return result
try:
print("Testing MCP3002 Inputs (Raw SPI)...")
while True:
adc_val_0 = read_mcp3002(0)
adc_val_1 = read_mcp3002(1)
# Convert to voltage (assuming 3.3V Vref)
voltage_0 = (adc_val_0 * 3.3) / 1023
voltage_1 = (adc_val_1 * 3.3) / 1023
print(f"CH0: {adc_val_0} ({voltage_0:.2f}V) | CH1: {adc_val_1} ({voltage_1:.2f}V)")
time.sleep(0.5)
except KeyboardInterrupt:
spi.close()
print("\nSPI Closed. Exiting...")
BUT WAIT! A WORD FROM OUR SPONSOR: PCBWAY

After getting your SBC set, you will probably want to design a PCB for your SBC then get it fabricated.
SPI--MCP4922
import spidev
import time
import sys
"""
you need linux app python3-dev.
sudo apt-get update
sudo apt-get install python3-dev
you also need spidev, to add with UV:
uv add spidev
"""
# --- Config ---
SPI_BUS = 0
SPI_DEVICE = 0 # Ensure your CS wire is on Pin 24 (CE0)
spi = spidev.SpiDev()
try:
spi.open(SPI_BUS, SPI_DEVICE)
spi.max_speed_hz = 1000000
spi.mode = 0b00 # Explicitly set Mode 0 (CPOL=0, CPHA=0)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
def send_dac(channel, value):
# Channel 0 = A, 1 = B
# Config bits: Buffered, 1x Gain, Active
# A: 0011 (0x3) | B: 1011 (0xB)
# Note: I changed bit 12 to '1' (Active) and bit 13 to '1' (1x Gain)
# The previous script used 0x7 and 0xF which is also valid but let's be explicit.
config = 0x3000 if channel == 0 else 0xB000
data = config | (value & 0xFFF)
upper = (data >> 8) & 0xFF
lower = data & 0xFF
spi.xfer2([upper, lower])
print("--- DEBUG MODE: Slow Toggle ---")
print("Both channels should switch between 0V and 3.3V every 3 seconds.")
print("Measure Pin 14 (A) and Pin 10 (B) now.")
try:
while True:
print("Writing 0V (Low)...")
send_dac(0, 0) # Channel A -> 0
send_dac(1, 0) # Channel B -> 0
time.sleep(3)
print("Writing 3.3V (High)...")
send_dac(0, 4095) # Channel A -> Max
send_dac(1, 4095) # Channel B -> Max
time.sleep(3)
except KeyboardInterrupt:
spi.close()
import spidev
import time
import sys
"""
you need linux app python3-dev.
sudo apt-get update
sudo apt-get install python3-dev
you also need spidev, to add with UV:
uv add spidev
"""
# --- Configuration ---
SPI_BUS = 0
SPI_DEVICE = 0 # Uses CE0 (Pin 24)
SPI_SPEED = 4000000 # 4 MHz (Plenty fast for Python)
# MCP4922 Configuration Bits
# Bit 15: 0=A, 1=B
# Bit 14: Buf (1=Buffered)
# Bit 13: Gain (1=1x, 0=2x)
# Bit 12: SHDN (1=Active, 0=Shutdown)
# Config for Channel A: 0 1 1 1 (0x7)
# Note: Unbuffered (0) is fine too, but Buffered (1) drives loads better.
config_A = 0x7000
# Config for Channel B: 1 1 1 1 (0xF)
config_B = 0xF000
# DAC Resolution
DAC_MAX = 4095
TABLE_SIZE = 2048
# --- Setup SPI ---
spi = spidev.SpiDev()
try:
spi.open(SPI_BUS, SPI_DEVICE)
spi.max_speed_hz = SPI_SPEED
except Exception as e:
print(f"Error opening SPI: {e}")
sys.exit(1)
def create_triangle_table(steps):
"""Generates a lookup table for a 0-4095-0 triangle wave."""
table = []
half_steps = steps // 2
# Rising (0 -> 4095)
for i in range(half_steps):
val = int((i / half_steps) * DAC_MAX)
table.append(val)
# Falling (4095 -> 0)
for i in range(half_steps):
val = int((1 - (i / half_steps)) * DAC_MAX)
table.append(val)
return table
def write_dac(channel_config, value):
"""
Sends a value to the MCP4922.
channel_config: config_A or config_B base bits
value: 0-4095
"""
# 1. Combine Config bits with 12-bit Value
# The command is 16 bits total.
# Top 4 bits are Config, Bottom 12 are Data.
data = channel_config | (value & 0xFFF)
# 2. Split into two bytes
upper_byte = (data >> 8) & 0xFF
lower_byte = data & 0xFF
# 3. Send via SPI
# xfer2 keeps CS low for the transaction
spi.xfer2([upper_byte, lower_byte])
def benchmark_spi(samples=1000):
"""Checks how fast Python can drive the SPI loop."""
print("Benchmarking SPI speed...")
start = time.time()
for _ in range(samples):
# Write both channels to simulate real load
write_dac(config_A, 2048)
write_dac(config_B, 2048)
duration = time.time() - start
rate = samples / duration
print(f"Max Update Rate (Dual Channel): {rate:.1f} Hz")
return rate
def run_quadrature_waves(frequency):
wave_table = create_triangle_table(TABLE_SIZE)
# Measure system speed to calculate step size
max_rate = benchmark_spi()
# Calculate Step Size
# We need to traverse 'frequency' full tables per second.
step_size = (TABLE_SIZE * frequency) / max_rate
# Calculate Phase Offset (90 degrees = 1/4 table)
offset_90 = TABLE_SIZE // 4
print(f"\n--- Generating {frequency} Hz Waves (90 deg offset) ---")
print("Connect Scope to Pin 14 (Ch A) and Pin 10 (Ch B)")
print(f"Step Size: {step_size:.4f}")
current_idx = 0.0
try:
while True:
# Calculate Indices
idx_a = int(current_idx) % TABLE_SIZE
idx_b = (idx_a + offset_90) % TABLE_SIZE
# Lookup Values
val_a = wave_table[idx_a]
val_b = wave_table[idx_b]
# Write to DACs
write_dac(config_A, val_a)
write_dac(config_B, val_b)
# Increment
current_idx += step_size
except KeyboardInterrupt:
print("\nStopping...")
write_dac(config_A, 0)
write_dac(config_B, 0)
spi.close()
if __name__ == "__main__":
try:
freq = float(input("Enter frequency (Hz): "))
run_quadrature_waves(freq)
except ValueError:
print("Invalid number.")





















