Signal generator applet
Hi. It would be cool to have simple signal generator applet. I'm not sure about DACs on the Glasgow, but even a square signal from a normal IO would be great.
I found the control-servo applet which generates a limited PWM signal. This is a great start, but it would be great to have more freedom. Some features I'm thinking about:
- Simple square signal defined by providing a pulse with, a pause with and a pin.
- "Replaying" a signal: same as 1. but a string of 1 and 0 can be provided with 1 being a high signal using the pulse with and a 0 being a low signal using the pause width.
- Same as 1. or 2. but make it parallel. This would allow to specify multiple pins and to simulate a parallel bus signal.
This would also be great for some quick and rudimentary bit banging tests.
My concern here is the degree of generality. Ultimately, you could consider any applet a type of "signal generator" since that's what Glasgow does. I think I can't accept such an applet upstream without a clearly defined and justified scope.
Ultimately I also want it to be easy enough to add new applets that you wouldn't have to configure some monster with dozens of features to generate a signal similar to what you want; you would write a little bit of gateware that is exactly what you want.
Hi. It would be cool to have simple signal generator applet. I'm not sure about DACs on the Glasgow, but even a square signal from a normal IO would be great.
I found the
control-servoapplet which generates a limited PWM signal. This is a great start, but it would be great to have more freedom. Some features I'm thinking about:1. Simple square signal defined by providing a pulse with, a pause with and a pin. 2. "Replaying" a signal: same as 1. but a string of 1 and 0 can be provided with 1 being a high signal using the pulse with and a 0 being a low signal using the pause width. 3. Same as 1. or 2. but make it parallel. This would allow to specify multiple pins and to simulate a parallel bus signal.This would also be great for some quick and rudimentary bit banging tests.
A simple sig gen is one of the basic features, I would expect. Just dial in frequency and amplitude, maybe waveform (sine, square, triangle), that's it.
Glasgow can't generate sine or triangle without an external LPF.
Glasgow can't generate sine or triangle without an external LPF.
Even a square gen would actually help me.
I also have an Analog Discovery 2, which I use a lot. I mostly use it as a digital signal analyser and protocol decoder, but I like the 'scope functionality, and the LCR addon is useful, though the dedicated ones are easier. I do not use the signal generator very much, but it is used for the LCR addon.
The Glasgow has more output capability, and its single channel analog input has a much more useful voltage range than the Analog discovery 2.
I think a signal generator and single channel scope are natural targets for the Glasgow - even if they are low frequency.
Even a square gen would actually help me.
Something scoped to a square gen would work. How do you imagine controlling it? Do you want to change the signal parameters on the fly? Do you want cycle-accurate event scheduling or anything like that?
I think a signal generator and single channel scope are natural targets for the Glasgow - even if they are low frequency.
An arbitrary waveform generator would need an analog frontend, and this is actually already covered: audio-dac plus a filter or a more complex frontend gives you that capability.
A binary pulse train generator can get fairly complex if you want multi-channel synchronization, on-the-fly updates, or things like that (you could write a PhD on this topic alone) so without a well defined scope I don't want to have one in-tree.
The analog input has something like 1 kHz of bandwidth, which is more of a "multimeter" width than a "scope" width, even for a toy scope. I don't think we currently have infrastructure to utilize even that at full capacity, and I'm not sure that infrastructure would be easy to fit into the FX2 either. Namely: I don't feel that the complexity of adding an entire extra USB interrupt endpoint just for that is justified (we're tight on space in the FX2 memory), and while you could use the FPGA to poll the internal I2C bus by going multi-master, this can potentially interfere with the overvoltage/overcurrent alert feature so I'm hesitant to rely on that either. (Overvoltage/overcurrent turns off Vio of the same port via a diode, but across ports the FX2 needs to be involved. Plus any issues with the I2C bus currently result in inscrutable behavior.)
Even a square gen would actually help me.
Something scoped to a square gen would work. How do you imagine controlling it? Do you want to change the signal parameters on the fly? Do you want cycle-accurate event scheduling or anything like that?
I would try to keep it as simple as possible: Just one single channel, frequency and amplitude as parameters, without any changes during runtime, that's it.
Glasgow does not have per-pin amplitude control. (You can control it indirectly via the port voltage, but our command line option system isn't really currently flexible enough to express the parameters as "frequency plus amplitude", although arguably it should be.)
Other than that sounds fine.
Glasgow does not have per-pin amplitude control.
As long as there is only a single channel in use, that should be fine.
As long as there is only a single channel in use, that should be fine.
A single channel per port (you can have two!). To be clear, I never wanted to imply this couldn't work electrically, this is purely a user interface limitation.
You can use GPIO to simulate the output of square wave signals
I wrote a portable script for him
Just modify the relevant parameters and run it.
At the same time, because of the hardware components, you can only use it to generate digital signals, collect square waves, pulses, clocks, digital communication signals (such as SPI, I2C, UART, etc.). It currently cannot initialize the processing of analog signals. For this reason, my current solution is more inclined to enable the LVDS interface + external module
import subprocess
import time
# Set pins and cycles
pin = "A0"
voltage = "3.3"
period_s = 1 / 1000 # 1kHz = 1ms total cycle
duty = 0.5 # 50%
high_time = period_s * duty
low_time = period_s * (1 - duty)
# Initialized to 3.3V
subprocess.run(["glasgow", "run", "control-gpio", "--pins", pin, "-V", voltage])
print("Outpute 1kHz square wave,Ctrl+C Stop")
try:
while True:
subprocess.run(["glasgow", "run", "control-gpio", "--pins", pin, f"{pin}=1"])
time.sleep(high_time)
subprocess.run(["glasgow", "run", "control-gpio", "--pins", pin, f"{pin}=0"])
time.sleep(low_time)
except KeyboardInterrupt:
print("\nStopped PWM Outpute")
subprocess.run(["glasgow", "run", "control-gpio", "--pins", pin, f"{pin}=0"])
It should be noted that the maximum I/O voltage is 3.3V, so the amplitude cannot exceed 3.3. Another interesting thing I found (a problem I found in the logic analyzer) is that when the frequency is too large, the chip will overflow, which means that high-frequency or high-bandwidth waveforms cannot be generated.
This code will not output a 1 kHz square wave. Did "AI" write it for you?
This code will not output a 1 kHz square wave. Did "AI" write it for you?
Ah, that's right. I only know it is a square wave (0 and 1) I use this program to make a running light example
Ah, makes sense. Do you have an oscilloscope? It's very useful when starting to work with electronics. Even a $20 Aliexpress oscilloscope (probably even cheaper in China) would be really useful.
Ah, makes sense. Do you have an oscilloscope? It's very useful when starting to work with electronics. Even a $20 Aliexpress oscilloscope (probably even cheaper in China) would be really useful.
I use the A0 interface to control the LED
It seems that it does not meet the standard, and its frequency is 0.(embarrassing moment)
@mayjack0312 I would recommend using the script mode, which will let you do "PWM" like this at maybe 100 Hz (but not reliably). In order to reliably output a square wave at 1 kHz (or faster) you will need to write some gateware.
@mayjack0312 I would recommend using the script mode, which will let you do "PWM" like this at maybe 100 Hz (but not reliably). In order to reliably output a square wave at 1 kHz (or faster) you will need to write some gateware.
OK, I will study the document. Thank you
@mayjack0312 Once you try that out, I recommend copying examples/boilerplate.py into a new applet and making it do PWM properly. Here is the gateware you can use as a starting point:
class PulseComponent(wiring.Component):
period: In(16)
def __init__(self, ports):
self._ports = ports
super().__init__()
def elaborate(self, platform):
m = Module()
m.submodules.out_buffer = out_buffer = io.Buffer("o", self._ports.out)
count = Signal.like(period)
with m.If(count == 0):
m.d.sync += out_buffer.o.eq(~out_buffer.o)
m.d.sync += count.eq(self.period)
with m.Else():
m.d.sync += count.eq(count - 1)
return m
@mayjack0312 Once you try that out, I recommend copying
examples/boilerplate.pyinto a new applet and making it do PWM properly. Here is the gateware you can use as a starting point:class PulseComponent(wiring.Component): period: In(16)
def __init__(self, ports): self._ports = ports super().__init__() def elaborate(self, platform): m = Module() m.submodules.out_buffer = out_buffer = io.Buffer("o", self._ports.out) count = Signal.like(period) with m.If(count == 0): m.d.sync += out_buffer.o.eq(~out_buffer.o) m.d.sync += count.eq(self.period) with m.Else(): m.d.sync += count.eq(count - 1) return m
OK
@mayjack0312 Once you try that out, I recommend copying
examples/boilerplate.pyinto a new applet and making it do PWM properly. Here is the gateware you can use as a starting point:class PulseComponent(wiring.Component): period: In(16)
def __init__(self, ports): self._ports = ports super().__init__() def elaborate(self, platform): m = Module() m.submodules.out_buffer = out_buffer = io.Buffer("o", self._ports.out) count = Signal.like(period) with m.If(count == 0): m.d.sync += out_buffer.o.eq(~out_buffer.o) m.d.sync += count.eq(self.period) with m.Else(): m.d.sync += count.eq(count - 1) return m
The test results are as follows
>>> await test_frequency_limits()
=== Frequency Limit Test ===
Testing different frequencies to find practical limit...
Testing 10Hz...
Target: 10Hz, 50.0% duty
Period: 100.000ms, High: 50.000ms, Low: 50.000ms
Generated 29 cycles in 3.06s
Actual frequency: 9.5Hz
Error: 5.3%
Test completed in 3.06s
Testing 15Hz...
Target: 15Hz, 50.0% duty
Period: 66.667ms, High: 33.333ms, Low: 33.333ms
Generated 42 cycles in 3.07s
Actual frequency: 13.7Hz
Error: 8.8%
Test completed in 3.07s
Testing 20Hz...
Target: 20Hz, 50.0% duty
Period: 50.000ms, High: 25.000ms, Low: 25.000ms
Generated 54 cycles in 3.00s
Actual frequency: 18.0Hz
Error: 10.1%
Test completed in 3.00s
Testing 25Hz...
Target: 25Hz, 50.0% duty
Period: 40.000ms, High: 20.000ms, Low: 20.000ms
Generated 66 cycles in 3.01s
Actual frequency: 21.9Hz
Error: 12.4%
Test completed in 3.01s
Testing 30Hz...
Target: 30Hz, 50.0% duty
Period: 33.333ms, High: 16.667ms, Low: 16.667ms
Generated 77 cycles in 3.01s
Actual frequency: 25.6Hz
Error: 14.8%
Test completed in 3.01s
Testing 35Hz...
Target: 35Hz, 50.0% duty
Period: 28.571ms, High: 14.286ms, Low: 14.286ms
Generated 87 cycles in 3.02s
Actual frequency: 28.8Hz
Error: 17.8%
Test completed in 3.02s
Testing 40Hz...
Target: 40Hz, 50.0% duty
Period: 25.000ms, High: 12.500ms, Low: 12.500ms
Generated 97 cycles in 3.03s
Actual frequency: 32.0Hz
Error: 20.0%
Test completed in 3.03s
Testing 45Hz...
Target: 45Hz, 50.0% duty
Period: 22.222ms, High: 11.111ms, Low: 11.111ms
Generated 104 cycles in 3.01s
Actual frequency: 34.6Hz
Error: 23.2%
Test completed in 3.01s
Testing 50Hz...
Target: 50Hz, 50.0% duty
Period: 20.000ms, High: 10.000ms, Low: 10.000ms
Generated 118 cycles in 3.00s
Actual frequency: 39.3Hz
Error: 21.4%
Test completed in 3.00s
Testing 55Hz...
Target: 55Hz, 50.0% duty
Period: 18.182ms, High: 9.091ms, Low: 9.091ms
Generated 120 cycles in 3.02s
Actual frequency: 39.7Hz
Error: 27.8%
Test completed in 3.02s
Testing 60Hz...
Target: 60Hz, 50.0% duty
Period: 16.667ms, High: 8.333ms, Low: 8.333ms
Generated 133 cycles in 3.01s
Actual frequency: 44.1Hz
Error: 26.4%
Test completed in 3.01s
Testing 65Hz...
Target: 65Hz, 50.0% duty
Period: 15.385ms, High: 7.692ms, Low: 7.692ms
Generated 146 cycles in 3.02s
Actual frequency: 48.3Hz
Error: 25.7%
Test completed in 3.02s
Testing 70Hz...
Target: 70Hz, 50.0% duty
Period: 14.286ms, High: 7.143ms, Low: 7.143ms
Generated 150 cycles in 3.01s
Actual frequency: 49.8Hz
Error: 28.8%
Test completed in 3.01s
Testing 75Hz...
Target: 75Hz, 50.0% duty
Period: 13.333ms, High: 6.667ms, Low: 6.667ms
Generated 165 cycles in 3.02s
Actual frequency: 54.7Hz
Error: 27.1%
Test completed in 3.02s
Testing 80Hz...
Target: 80Hz, 50.0% duty
Period: 12.500ms, High: 6.250ms, Low: 6.250ms
Generated 167 cycles in 3.01s
Actual frequency: 55.6Hz
Error: 30.6%
Test completed in 3.01s
Testing 85Hz...
Target: 85Hz, 50.0% duty
Period: 11.765ms, High: 5.882ms, Low: 5.882ms
Generated 181 cycles in 3.02s
Actual frequency: 60.0Hz
Error: 29.4%
Test completed in 3.02s
Testing 90Hz...
Target: 90Hz, 50.0% duty
Period: 11.111ms, High: 5.556ms, Low: 5.556ms
Generated 189 cycles in 3.01s
Actual frequency: 62.7Hz
Error: 30.3%
Test completed in 3.01s
Testing 95Hz...
Target: 95Hz, 50.0% duty
Period: 10.526ms, High: 5.263ms, Low: 5.263ms
Generated 189 cycles in 3.01s
Actual frequency: 62.8Hz
Error: 33.9%
Test completed in 3.01s
Testing 100Hz...
Target: 100Hz, 50.0% duty
Period: 10.000ms, High: 5.000ms, Low: 5.000ms
Generated 205 cycles in 3.00s
Actual frequency: 68.3Hz
Error: 31.7%
Test completed in 3.00s
None
>>>
The following table can intuitively express the frequency bottleneck
| Target Frequency (Hz) | Target Period (ms) | High Time (ms) | Low Time (ms) | Actual Frequency (Hz) | Error (%) | Test Duration (s) | Generated Cycles |
|---|---|---|---|---|---|---|---|
| 10 | 100.000 | 50.000 | 50.000 | 9.5 | 5.1 | 3.06 | 29 |
| 15 | 66.667 | 33.333 | 33.333 | 13.7 | 8.5 | 3.06 | 42 |
| 20 | 50.000 | 25.000 | 25.000 | 18.1 | 9.7 | 3.04 | 55 |
| 25 | 40.000 | 20.000 | 20.000 | 22.0 | 11.9 | 3.04 | 67 |
| 30 | 33.333 | 16.667 | 16.667 | 25.7 | 14.4 | 3.04 | 78 |
| 35 | 28.571 | 14.286 | 14.286 | 28.5 | 18.7 | 3.02 | 86 |
| 40 | 25.000 | 12.500 | 12.500 | 32.7 | 18.2 | 3.03 | 99 |
| 45 | 22.222 | 11.111 | 11.111 | 35.0 | 22.2 | 3.03 | 106 |
| 50 | 20.000 | 10.000 | 10.000 | 40.0 | 19.9 | 3.02 | 121 |
| 55 | 18.182 | 9.091 | 9.091 | 41.1 | 25.4 | 3.02 | 124 |
| 60 | 16.667 | 8.333 | 8.333 | 45.0 | 24.9 | 3.02 | 136 |
| 65 | 15.385 | 7.692 | 7.692 | 48.9 | 24.8 | 3.01 | 147 |
| 70 | 14.286 | 7.143 | 7.143 | 49.8 | 28.8 | 3.01 | 150 |
| 75 | 13.333 | 6.667 | 6.667 | 54.8 | 27.0 | 3.01 | 165 |
| 80 | 12.500 | 6.250 | 6.250 | 55.8 | 30.3 | 3.01 | 168 |
| 85 | 11.765 | 5.882 | 5.882 | 60.5 | 28.9 | 3.01 | 182 |
| 90 | 11.111 | 5.556 | 5.556 | 62.9 | 30.1 | 3.00 | 189 |
| 95 | 10.526 | 5.263 | 5.263 | 62.8 | 33.9 | 3.01 | 189 |
| 100 | 10.000 | 5.000 | 5.000 | 68.6 | 31.4 | 3.00 | 206 |
Its optimal working range: 10-25Hz, the error is controlled within 12%. I think its stable upper limit is 70Hz.
The highest frequency it can generate is 173.6Hz, but the error is a bit too high.
Yes, the GPIO method is very limited. You need to use the gateware method to achieve good results. With it, you can generate frequencies up to 24 MHz, although the error gets higher the higher you go.
Yes, the GPIO method is very limited. You need to use the gateware method to achieve good results. With it, you can generate frequencies up to 24 MHz, although the error gets higher the higher you go.
"Good enough" It is mainly used as a universal burner ^_^
It was also found that the FPGA chip was limited by the crystal oscillator and could only reach a maximum of 48MHz
The GPIO example code is as follows, please run it using REPL
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Direct PWM Generator for Glasgow
Uses direct GPIO control for maximum efficiency
"""
import asyncio
import time
class DirectPWMController:
"""Direct PWM controller using minimal overhead"""
def __init__(self, iface, pin=0):
self.iface = iface
self.pin = pin
self.running = False
async def setup(self):
"""Setup pin as output"""
await self.iface.output(self.pin, False)
print(f"Pin {self.pin} configured as output")
async def generate_direct_pwm(self, freq, duty, duration=None, cycles=None):
"""
Generate PWM with direct control
Args:
freq: Target frequency in Hz
duty: Duty cycle (0.0-1.0)
duration: Duration in seconds (None for infinite)
cycles: Number of cycles (None for infinite)
"""
if not 0.0 <= duty <= 1.0:
raise ValueError("Duty cycle must be 0.0-1.0")
if freq <= 0:
raise ValueError("Frequency must be positive")
# Calculate timing
period = 1.0 / freq
high_time = period * duty
low_time = period * (1 - duty)
print(f"Target: {freq}Hz, {duty:.1%} duty")
print(f"Period: {period*1000:.3f}ms, High: {high_time*1000:.3f}ms, Low: {low_time*1000:.3f}ms")
self.running = True
start_time = time.time()
cycle_count = 0
try:
while self.running:
if duration and (time.time() - start_time) >= duration:
break
if cycles and cycle_count >= cycles:
break
# High level
await self.iface.set(self.pin, True)
await asyncio.sleep(high_time)
# Low level
await self.iface.set(self.pin, False)
await asyncio.sleep(low_time)
cycle_count += 1
except KeyboardInterrupt:
print("\nInterrupted by user")
finally:
self.running = False
await self.iface.set(self.pin, False)
duration = time.time() - start_time
actual_freq = cycle_count / duration if duration > 0 else 0
print(f"Generated {cycle_count} cycles in {duration:.2f}s")
print(f"Actual frequency: {actual_freq:.1f}Hz")
print(f"Error: {abs(actual_freq - freq) / freq * 100:.1f}%")
# Global controller
direct_pwm = None
async def init_direct_pwm(iface, pin=0):
"""Initialize direct PWM controller"""
global direct_pwm
direct_pwm = DirectPWMController(iface, pin)
await direct_pwm.setup()
return direct_pwm
async def direct_1khz(duty=0.5, duration=10):
"""Generate 1kHz PWM using direct method"""
if not direct_pwm:
print("PWM not initialized. Run init_direct_pwm(gpio_iface) first")
return
await direct_pwm.generate_direct_pwm(1000, duty, duration)
async def direct_custom(freq, duty=0.5, duration=10):
"""Generate custom frequency PWM using direct method"""
if not direct_pwm:
print("PWM not initialized. Run init_direct_pwm(gpio_iface) first")
return
await direct_pwm.generate_direct_pwm(freq, duty, duration)
# Test different frequencies to find the limit
async def test_frequency_limits():
"""Test different frequencies to find the practical limit"""
if not direct_pwm:
await init_direct_pwm(gpio_iface)
test_frequencies = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]
duty = 0.5
test_duration = 3 # 3 seconds per test
print("=== Frequency Limit Test ===")
print("Testing different frequencies to find practical limit...")
for freq in test_frequencies:
print(f"\nTesting {freq}Hz...")
start_time = time.time()
await direct_pwm.generate_direct_pwm(freq, duty, test_duration)
duration = time.time() - start_time
print(f"Test completed in {duration:.2f}s")
# Quick test
async def quick_direct_test():
"""Quick test with direct method"""
print("=== Quick Direct PWM Test ===")
await init_direct_pwm(gpio_iface)
await direct_1khz(0.5, 5)
if __name__ == "__main__":
print("Direct PWM Generator for Glasgow")
print("Usage:")
print("1. Initialize: await init_direct_pwm(gpio_iface)")
print("2. 1kHz test: await direct_1khz()")
print("3. Custom freq: await direct_custom(500, 0.25, 5)")
print("4. Frequency test: await test_frequency_limits()")
print("5. Quick test: await quick_direct_test()")
What's ProtoBlade?
What's ProtoBlade?
Oh, I changed the name of another device O(∩_∩)O ~