The transmitter outputs 6 PWM channels at 50Hz with each channel ranging from 1000us to 2000us. There are many waveform examples that can be found on the web. Here is a trace of all 6 channels from the transmitter:
A 1.0MHz clock will count the number of ticks present in "on" and "off" pulse and report the value in microseconds. If the "on" time is bigger than 2600us an error is reported, this maybe due to bad wiring or PWM line is locked high. Also, the same logic will be used to detect if the line is locked low, for example, if the transmitter is turned off, the PWM value is updated with 0 and error condition.
Here is an example of using this code to drive BetaFlight.
First StepsI use vscode for editing verilog code, there are several extensions available to support syntax highlighting. For a simulation environment, will use Icarus Verilog. I use Linux, here is a guide to compile and install. So, why use Icarus Verilog, (iverilog)?
- Can use $display, i.e. printf statements in verilog to help debug
- setup test environment to simulate errors, test basic operation
There are two files: pwm_decode.v which is the module that decodes one channel of PWM. pwm_decoder_tb.v is the testbench that drives pwm_decode.v. This follows the same general methodology as unit testing software.
Also, should have gtkwave installed. The test bench generates a gtkwave file, now it is possible to see the top level timing diagrams and low level (UUT) timing.
TestBenchThe test bench is very basic to show how to generate a clock, pwm signal and validate the measurement. Verlog has functional coverage support and will not be used here. In the code section is pwm_decode_tb.v and will be described here each block
`default_nettype none <--- Assist in detecting errors in verilog
`timescale 1 ns/10 ps // time-unit = 1 us, precision = 10 ps
module pwm_decode_tb; <--- name of the module
reg clk = 1; <---- declare the clock as reg
reg pwm_signal = 0; <---- declare the pwm_signal as reg
reg reset = 0; <-----declare the reset as reg
time currentTime = 0; ,---
wire pwm_complete;
wire [15:0]pwm_value;
localparam period = 10000000;
//Override our clock with 1Mhz clock
pwm_decode #(period) UUT( .i_clk(clk),
.i_pwm(pwm_signal),
.i_reset(reset),
.o_pwm_ready(pwm_complete),
.o_pwm_value(pwm_value));
The module name should match the file name. Define a clk, reset, and pwm_signal are defined as registers and initialized. The remaining are wires, driven by the pwm_decode module. The local clock will be 10MHz clock and is used as a parameter to pwm_decode module.
UUT (Unit under test) is defined as the pwm_decod, with the defined inputs and outputs.
always begin
#50 clk =!clk; <-- toggle clock line at 50ns rate
end
The next block is how the clock is generated.
initial begin
$dumpfile("pwm_decode_tb.vcd"); <-- generates a gtkwave file
$dumpvars(0,pwm_decode_tb); <-- dump the initial values into file
/* measure 1999us */
#0001000 pwm_signal <= 1; <--- wait 1000ns then set pwm_signal high
#2000000 pwm_signal <= 0; <--- wait 2000us with pwm_signal 1
/* measure 2600ms (guard time is 2600max), should result in ERROR*/
#0010000 pwm_signal <= 1;
currentTime = $realtime;
#2700000 pwm_signal <= 0;
/* measure 699us, should result in ERROR*/
#0001000 pwm_signal <= 1;
currentTime = $realtime;
#0700000 pwm_signal <= 0;
currentTime = $realtime;
/* should result in error locked low */
#2610000 $finish;
end
always @(posedge clk)
begin
if (pwm_complete) begin
if ( pwm_value & GUARD_ERROR) begin
$display("PWM ERROR %d us", (pwm_value & ~GUARD_ERROR));
end
else
$display("PWM length %d us", pwm_value);
end
end
endmodule
The initial block drives the main test loop generating a pwm_signal at various rates to exercise the UUT
In the PWM directory is a script file (simulate.sh) to compile, run and use gtkwave to display the waveforms. Here is the output:
VCD info: dumpfile pwm_decode_tb.vcd opened for output.
PWM length 1999us
PWM ERROR 2600us
PWM ERROR 699us
PWM ERROR 0us
GTKWave Analyzer v3.3.86 (w)1999-2017 BSI
[0] start time.
[8022000000] end time.
Verilog Code to Decode One ChannelThe decode uses a case statement as a way to implement a simple state machine. The states are:
- MEASURING_OFF = 0: measures the length of off, if the length of off is greater than 2600us then load an error and transition to MEASURING_COMPLETE. If the PWM is one, transition to MEASURING_ON.
- MEASURING_ON = 1: measures the length of on, if the length is greater than 2600us then load an error and transition to MEASURING_COMPLETE.
- MEASURING_COMPLETE=2, reset counters and transition back to MEASURING_OFF.
This project leverages the fast serial Hackster project. Create a PWM memory mapped interface using Quartus Platform designer.
- From Quartus menu select tools -> Platform Designer
- File dialog will appear select avalon_fast_serial.qsys the following system should be displayed (the GitHub project will already have the pwm_decoder included).
- Create an Avalon slave. Right click on New Component...
- Enter pwm_decode for both Name and display name.
- From the menu bar Templates->Add Avalon-MM Pipelined slave
- Next select on Signals & interfaces. Need to delete avs_so_write, avs_s0_writeadata. There is no need to support writes just reads. Also, only needs support 6 registers, so reconfigure the avs_s0_address for 3 bits. Also need to add a conduit for the 6 PWM inputs. The screen will look like this:
- Next need to create the file for the slave. Select Files, then Create File form Signals.
- Now, the pwm_decoder component is created, no double click on it and it will add it to the System Contents. Now, connect the reset, clock and avs_s0 slave interface. Also need to export the conduit_end.
- Next need to remap the pwm_decoder address space to 0x60. Select address Map and change the address.
- Generate HDL, use the bottom on the far lower left. How do you use this? From the top level menu select Generate ->Show instantiation Template.
Open fast_serial.v is the top level module, the above is cut and paste and update the connections, to PIO0 thru PIO5.
avalon_fast_serial u0 (
.clk_clk (CLK_C0_50Mhz),
.in_bytes_stream_ready (o_in_bytes_stream_ready),
.in_bytes_stream_valid (rx_ready),
.in_bytes_stream_data (rx_data),
.led_gpio_led (LED),
.out_bytes_stream_ready (!tx_busy),
.out_bytes_stream_valid (tx_write),
.out_bytes_stream_data (tx_data),
.pwm_pwm_1(PIO0),
.pwm_pwm_2(PIO1),
.pwm_pwm_3(PIO2),
.pwm_pwm_4(PIO3),
.pwm_pwm_5(PIO4),
.pwm_pwm_6(PIO5),
.reset_reset_n(r_reset));
Python CodeIn the Python directory there is an example script to access the Avalon slaves, LED and PWM. From Linux:
python3 avalon_loopback_serial.py -port /dev/ttyUSB0 -test pwm
Port is the ttyUSBx device the cyc1000 FPGA is connected and -test arg is just to print out the pwm value. You can move the sticks on the transmitter and see the values change.
NWJS APP
Added a simple nwjs html application to display PWM inputs from the transmitter in real time and control the LEDs on the CY1000. The html app reads the PWM at 100ms.
When the transmitter is turned off the PWM values and text update to OFF. Here is an attempt to video the the app.
Comments