MimasV2 – Audio tones & PWM

In this tutorial, we will be generating some audio tones using the FPGA.

OK. First thing is to understand what constitutes a tone and how it can be generated digitally. Basically, a tone is a periodic audio signal. A speaker or a earphone coverts an electrical waveform into an audio signal of the same frequency and proportionate amplitude. So, our job is to generate a voltage(analog) signal to drive the output device.

Also if a single analog output is generated, then it is called mono. If 2(or more) outputs are generated – one for each earphone lead/speaker, then it is called stereo.

We need a mechanism to convert the digital data in the FPGA to analog waveform. Ideally, we need a DAC(Digital to Analog covertor) to do the conversion. But the board lacks a dedicated DAC; hence we have to make do with a ‘PWM DAC’. mimasV2 pwm dac.PNG

As you can see. the FPGA pins are connected to 2 resistors and then to the output.

What is PWM?

PWM stands for pulse width modulation. Its a powerful technique used in a variety of ways. One popular use case is to use it generate a desired analog voltage.

MimasV2 pwm waveform.jpg

PWM is a periodic train of square pulses. The frequency of the pulses are fixed but the pulse width can vary between pulses. Lets say the high state is expressed as a voltage of +3.3v and zero state is 0v. If we compute the average voltage a cycle, it is,

Vavg = (3.3*Ton + 0*Toff)/period = 3.3 * Ton/period = 3.3*DC

where, DC is the fractional duty cycle = Ton/(Ton+Toff) = Ton/period

Thus you can control the average voltage output by controlling the Ton period. Thus if Ton is a quarter of a period, then Vavg = 3.3/4 = 0.825v.

So, how is it practically implemented? In a digital system, the Ton can take only discrete steps. Lets say we have Ton as a 8bit value and a 8bit counter register. The counter counts from 0-255 and then back to 0 and so on(free running). The period of the counter register is the PWM period. MimasV2 pwm counter.jpg

We can have a digital comparator  which outputs ‘1’ if Ton<counter and ‘0’ if Ton>=counter. The Ton register can be set to any arbitrary value from 0(100% duty cycle) to 255(0% duty cycle). This is visualized in the picture above. The output of the comparator can be routed to an FPGA pin to the stereo jack.

In our system, there is just a slight modification. The conditions for the ‘1’ and ‘0’ are simply reversed.

What is sample rate?

The sample rate is the maximum rate at which the digital data samples for the audio ouput can be changed. That is, every sample period, a new data value can be put into the PWM comparator (Ton register). There are some standard audio sample rates one of which is 44Khz(which we will be using).

So we can put a new value to the PWM comparator reg every 1/44KHz = 22.727us. In this case our PWM frequency is the same as Sample rate = 44Khz.

We will end up with an ‘average’ waveform like this:

MimasV2 pwm sine wave 2

Notice that it changes in steps every sample period. Ideally for audio we prefer a continuous signal without discontinuities. To improve the audio quality we can do 2 things:

  1. one is to increase the sampling rate(and also PWM freq) so that the steps can be made more finer, making it closer to continuous waveform.
  2. Increase the PWM frequency keeping the sample rate same. An increased sample rate poses increased data processing loads on the system. But no such cost is incurred if you only increase the PWM frequency. This will make the output analog values closer to what you intended.

We are going to go with option 2. I chose the PWM frequency = 10*sample rate. Hence PWM freq = 440KHZ.

Also you might be wondering why PWM is called ‘modulation’. This because the message signal(our audio) is represented by the changing pulse widths of the high frequency carrier(PWM pulse train).

Finally, you should know that even though we have increased our PWM frequency ten fold, there may some audible artifacts because of the discontinuities in the output signal. To smoothen this out, a not so straight forward low pass filter is employed. The earphones/speakers internally contain capacitive components to block DC voltages. The resistor on the FPGA pin combined with this capacitance, form a simple RC low pass filter. The final output may look something like this.

MimasV2 pwm sine wave 1.jpg

 

Ok. With that theory base, the implementation is straight forward.

The system clock is 100MHz. We need to divide this with suitable counters to generate the PWM counter and the sample clock.

pwm divisor = 100M/440K = 227 (this is PWM counter period)

sample clock divisor = 100M/44K = 2273

The PWM counter counts from 0 to 226 and back to 0 and so on. Every sample clock, a new value is loaded into the Ton comparator reg.

So what values are we going to load into the Ton reg? A very crude sine wave consists of rising phase and falling phase. So I start with 0 and increment it by an amount ‘sampleInc’. Once it reaches 226, then it starts decrementing by ‘sampleInc’ amount till it reaches 0 and so on. The sampleInc value, I have kept it programmable through the serial interface. Note that higher sampleInc means the sine cycles get completed sooner – higher frequencies. The sampleInc gets displayed on the 7 segment display.

Both the left and right channels are driven by the same signal.

 

top_module.v


`timescale 1ns / 1ps

module top_module(L,R,a,b,c,d,e,f,g,h,e1,e2,e3,tx,rx,clk,reset);

output L,R;
output a,b,c,d,e,f,g,h,e1,e2,e3,tx;
input rx,clk,reset;

localparam [1:0] idle=2'b00, data1=2'b01, data2=2'b10, data3=2'b11;

// Signals for UART submodule
reg rd_uart = 1'b0;
reg wr_uart = 1'b0;
reg [7:0] data_out = 0 ;
wire [7:0] data_in ;
wire full ;
wire empty ;
reg [7:0] data_rx = 0 ;
reg [1:0] rx_state = 0 ;

reg [9:0] disp_num = 0 ;
reg [9:0] disp_num1 = 1'd1;
wire [3:0] dig0,dig1,dig2 ;
reg [3:0] currentDig ;
reg [1:0] digCnt = 2'd0 ;
reg dig0en,dig1en,dig2en;
assign e1=dig2en; assign e2=dig1en; assign e3=dig0en;

reg [15:0] timer_cnt = 0 ;
reg [7:0] pwmCnt = 0 ;
reg [11:0] sampCnt = 0 ;
reg sampClk = 0 ;
reg [7:0] sampleVal = 0 ;
reg [7:0] sampleInc = 8'd1;
reg IncDec = 1'b1;

assign L = sampleVal >= pwmCnt;
assign R = sampleVal >= pwmCnt;

bin2BCD v1({dig2,dig1,dig0},disp_num1);
BCDto7Seg v2(a,b,c,d,e,f,g,h,currentDig);

// DIVISOR = 326 for 19200 baudrate, 100MHz sys clock
uart #( .DIVISOR (9'd326),
.DVSR_BIT (4'd9) ,
.Data_Bits (4'd8) ,
.FIFO_Add_Bit (3'd4)
) uart ( .clk (clk),
.rd_uart (rd_uart) ,
.reset (reset) ,
.rx (rx) ,
.w_data (data_out) ,
.wr_uart (wr_uart) ,
.r_data (data_in) ,
.rx_empty (empty) ,
.tx (tx) ,
.tx_full (full)
);

// Timer logic
always @(posedge clk)
begin
if(!reset)
timer_cnt <= 0;
else
timer_cnt <= timer_cnt + 1'b1;
end

always @(posedge clk or negedge reset)
begin
if(!reset)
sampCnt<=0;
else if(sampCnt==12'd2272)
begin sampClk<=1'b1; sampCnt<=0; end
else
begin sampCnt<=sampCnt+1'b1; sampClk<=1'b0; end
end

always @(posedge clk or negedge reset)
begin
if(!reset)
pwmCnt<=0;
else if(pwmCnt==8'd226)
pwmCnt<=0;
else
pwmCnt=sampleInc)
sampleVal<=sampleVal+sampleInc;
else
IncDec=sampleInc)
sampleVal<=sampleVal-sampleInc;
else
IncDec<=1'b1;
end
end

always @(posedge timer_cnt[10] or negedge reset)
begin
if(!reset)
begin
rx_state<=idle;
disp_num<=0;
disp_num1<=10'd1;
sampleInc<=8'd1;
end
else
begin
if(!empty)
begin
rd_uart <= 1'b1;
data_rx ="0" && data_rx<="9") begin rx_state<=data1; disp_num="0" && data_rx<="9") begin rx_state<=data2; disp_num<=4'd10*disp_num + (data_rx-"0"); end
else if(data_rx=="\r") begin
rx_state=10'd1 && disp_num<=10'd100) begin disp_num1<=disp_num;sampleInc<=disp_num[7:0]; end
disp_num<=0; end
else begin rx_state<=idle; disp_num="0" && data_rx<="9") begin rx_state<=data3; disp_num<=4'd10*disp_num + (data_rx-"0"); end
else if(data_rx=="\r") begin
rx_state=10'd1 && disp_num<=10'd100) begin disp_num1<=disp_num;sampleInc<=disp_num[7:0]; end
disp_num<=0; end
else begin rx_state<=idle; disp_num<=0; end
end
data3: begin
if(data_rx!="\r") begin rx_state<=idle; disp_num<=0; end
else begin
rx_state=10'd1 && disp_num<=10'd100) begin disp_num1<=disp_num;sampleInc<=disp_num[7:0]; end
disp_num<=0; end
end
endcase
end
else
rd_uart<=1'b0;
end
end

always @(posedge timer_cnt[15]) begin
case (digCnt)
2'd0: begin
digCnt <= 2'd1;
dig2en <= 1'b1;dig1en <= 1'b1;dig0en <= 1'b0;
currentDig <= dig0;
end
2'd1: begin
digCnt <= 2'd2;
if(dig1==4'd0 & dig2==4'd0) begin dig2en <= 1'b1;dig1en <= 1'b1;dig0en <= 1'b1; end
else begin dig2en <= 1'b1;dig1en <= 1'b0;dig0en <= 1'b1; currentDig <= dig1; end
end
2'd2: begin
digCnt <= 2'd0;
if(dig2==4'd0) begin dig2en <= 1'b1;dig1en <= 1'b1;dig0en <= 1'b1; end
else begin dig2en <= 1'b0;dig1en <= 1'b1;dig0en <= 1'b1; currentDig <= dig2; end
end
endcase
end

endmodule

 

audioTest_ucf.ucf


#####################################################################
# Clock 100 MHz #
#####################################################################
CONFIG VCCAUX = "3.3" ;

NET "clk" LOC = V10 | IOSTANDARD = LVCMOS33 | PERIOD = 100MHz ;

#####################################################################
# Reset #
#####################################################################
NET "reset" LOC = M16 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST | PULLUP; #SW3
NET "reset" CLOCK_DEDICATED_ROUTE = FALSE;

#####################################################################
# Uart #
#####################################################################
NET "tx" LOC = B8 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ;
NET "rx" LOC = A8 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ;

NET "L" LOC = B16 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ;
NET "R" LOC = A16 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ;

NET "a" LOC = A3 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #a IO_L4N_0 Sch = 7Seg_0
NET "b" LOC = B4 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #b IO_L5P_0 Sch = 7Seg_1
NET "c" LOC = A4 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #c IO_L5N_0 Sch = 7Seg_2
NET "d" LOC = C4 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #d IO_L1N_VREF_0 Sch = 7Seg_4
NET "e" LOC = C5 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #e IO_L6P_0 Sch = 7Seg_5
NET "f" LOC = D6 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #f IO_L3P_0 Sch = 7Seg_6
NET "g" LOC = C6 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #g IO_L3N_0 Sch = 7Seg_7
NET "h" LOC = A5 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; #dot IO_L6N_0 Sch = 7Seg_3

NET "e1" LOC = B3 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; # IO_L4P_0 Sch = 7Seg_1en #Enables for Seven Segment
NET "e2" LOC = A2 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; # IO_L2N_0 Sch = 7Seg_2en
NET "e3" LOC = B2 | IOSTANDARD = LVCMOS33 | DRIVE = 8 | SLEW = FAST ; # IO_L2P_0 Sch = 7Seg_3en

 

I haven’t provided the UART and 7 segment files. Please refer to previous tutorials for them and add them to the project.

You can connect an earphone or a speaker to the audio jack to listen to the audio tone.

You can find the entire project folder in zip format in my GitHub repo:

https://github.com/Anirudh-R/Mimas-V2/blob/master/audioTest_14apr17.zip

To conclude, we have understood about PWM and how it can be used to generate an audio tone. PWM has various uses in control applications. They can be used to control servos, brushless motors and to control the speed of DC motors. They are also used in sine wave inverters. Thus it finds extensive use in industries. Normally a simple microcontroller can generate the required PWM signals to control few motors. There are some special peripherals for microcontrollers that can generate accurate PWM widths for precise motor control. When multiple motors or multi axis motors which require precise control are needed, then it may favour the use of FPGAs.

Till next time,

Anirudh

 

 

Author: anirudhr

Electronics hobbyist with special interest in the field of Software Defined Radios, FPGAs and DSP. Beginner but always ready to learn new stuff. Nothing like Hands on experience to teach you something.

Leave a comment