Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

Click the blue text

Follow us

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

This article introduces the design and implementation of a local control DDS arbitrary waveform generator based on STM32 and FPGA. The project successfully generates adjustable waveforms (sine, triangle, square), frequencies (DC-5MHz), and amplitudes (0.1V-1V) using a high-speed DAC combined with FPGA internal DDS logic, and displays waveform parameters in real-time on an OLED screen. The author shares the entire process from hardware selection, design ideas to code implementation, and discusses the challenges encountered in the project and directions for improvement, providing a highly valuable practical case for electronics enthusiasts and engineers.

Project Introduction

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

1. Generate adjustable waveforms (sine, triangle, square), adjustable frequencies (DC-), and adjustable amplitudes using the high-speed DAC on the board combined with the internal DDS logic of the FPGA.

2. The frequency range of the generated analog signal is DC-5MHz, with a tuning precision of 1Hz.

3. The amplitude of the generated analog signal is up to 1Vpp, with a tuning range of 0.1V-1V.

4. Display the current waveform shape, frequency, and amplitude on the OLED.

5. Use the onboard rotary encoder and buttons to switch waveforms and adjust parameters.

Design Ideas

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

1. Hardware and Compilation Environment

This project uses the Lattice ICE40UP5K FPGA and STM32G031 MCU, along with buttons, a rotary encoder, and an OLED display connected to the STM32, and a high-speed DAC connected to the FPGA. The main development environments used are STM32CubeMX, Keil5, and Radiant.

2. Design Block Diagram

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

As shown in the figure, the buttons and rotary encoder are connected to the STM32 to input waveform signals, the OLED screen displays information, and the FPGA is connected to the high-speed DAC to generate waveform signals. The STM32 and FPGA are connected via SPI for signal transmission.

Code Sharing

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

1. Button Input Section

Two buttons and a rotary encoder are used here. Due to the limited resources of buttons, each button is designed to have a single function and be spaced apart. The main inputs needed are the frequency, amplitude, and waveform number, with a frequency range of 0 to 5000000Hz, a precision of 1Hz, an amplitude range of 0 to 1V, and a precision of 0.1V. Button 1 is responsible for switching modes for amplitude, frequency, and waveform, while Button 2 selects each bit of the frequency. The rotary encoder is used for digital input of frequency and amplitude, and to select the waveform. When Button 1 is pressed for the fourth time, the STM32 sends all data to the FPGA and displays the waveform.

Button 1:

if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_12)==0){                  key1_num++;

Button 2:

if(GPIO_Pin == KEY2_Pin)// Second layer button frequency bit selection  {       delay(10);      if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11)==0&&key1_num==2){           num[2]=0;            if(wei>=7)              wei=0;            else              wei++;      }  

Rotary Encoder:

if(GPIO_Pin == A_state_Pin)  {        HAL_TIM_Base_Start_IT(&htim2);      if((wei==7&&&num[2]>=5)||num[2]>=9) num[2]=0;      if(num[3]>=3) num[3]=0;      if(num[1]>=10) num[1]=0;                    if(flag==1&&&num[key1_num]>=1)              {                  num[key1_num]--;              }              elseif(flag==2)              {                num[key1_num]++;               }              switch(key1_num){                     case1:amp=num[1];  break;                                                   case2:freq[wei]=num[2];                            freq_sum=freq[7]*1000000+freq[6]*100000+freq[5]*10000+freq[4]*1000+freq[3]*100+freq[2]*10+freq[1];                              data1=freq[2]*10+freq[1];// Combine frequency data to be sent after pressing Button 2                            data2=freq[4]*10+freq[3];                            data3=freq[6]*10+freq[5];                            data4=freq[7];                              break;                     case3:wave=num[3];                                                             break;                     default: break;                                  }            flag=0;                              }  

For debouncing, the two buttons are debounced simply using a custom delay function, while the rotary encoder uses a timer, and the direction of rotation is determined in the timer interrupt. The direction of rotation is primarily judged by the timing of the low-level signals generated at the A and B ends.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){  if(htim==(&htim2))  {        if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)==0)       {          if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1)==0)           flag=1;                  else                       flag=2;                      }       HAL_TIM_Base_Stop_IT(&htim2);  }   } 

2. OLED Interface

For the OLED part, I directly used the existing driver code with slight modifications, configuring MOSI and SCK in SPI2 to send data, and software simulating the chip select signal. The interface includes a downward line for better positioning of the button input location.

void show(void){           OLED_ShowString(8,32,(uint8_t *)"WARE:",16,1);       OLED_ShowString(8,0,(uint8_t *)"AMP:",16,1);       OLED_ShowString(8,16,(uint8_t *)"freq:",16,1);         OLED_ShowNum(40,0,amp/10,1,16,1);           OLED_ShowString(48,0,(uint8_t *)".",16,1);              OLED_ShowNum(54,0,amp%10,1,16,1);       OLED_ShowString(104,0,(uint8_t *)"V",16,1);              OLED_ShowNum(48,16,freq_sum,7,16,1);       OLED_ShowString(104,16,(uint8_t *)"HZ",16,1);       switch(wave){         case0:OLED_ShowString(56,32,(uint8_t *)"DC      ",16,1);break;       case1:OLED_ShowString(56,32,(uint8_t *)"square  ",16,1);break;           case2:OLED_ShowString(56,32,(uint8_t *)"triangle",16,1);break;       case3:OLED_ShowString(56,32,(uint8_t *)"sine    ",16,1);break;         default:break;       }                     switch(key1_num){         case1:OLED_DrawLine(8,15,31,15,1);break;         case2:{OLED_DrawLine(8,31,39,31,1);OLED_DrawLine(8,15,31,15,0);break;}         case3:{OLED_DrawLine(8,31,39,31,0);OLED_DrawLine(8,47,39,47,1);break;}                    default:{OLED_DrawLine(8,15,31,15,0);OLED_DrawLine(8,31,39,31,0);OLED_DrawLine(8,47,39,47,0);break;}       }         if(key1_num==2){          switch(wei){               case7:{OLED_DrawLine(56,31,63,31,0);OLED_DrawLine(48,31,55,31,1);break;}               case6:{OLED_DrawLine(64,31,71,31,0);OLED_DrawLine(56,31,63,31,1);break;}               case5:{OLED_DrawLine(72,31,79,31,0);OLED_DrawLine(64,31,71,31,1);break;}                             case4:{OLED_DrawLine(80,31,87,31,0);OLED_DrawLine(72,31,79,31,1);break;}                             case3:{OLED_DrawLine(88,31,95,31,0);OLED_DrawLine(80,31,87,31,1);break;}                             case2:{OLED_DrawLine(96,31,103,31,0);OLED_DrawLine(88,31,95,31,1);break;}                             case1:{OLED_DrawLine(96,31,103,31,1);break;}                    default:{OLED_DrawLine(48,31,55,31,0);break;}             }       }              OLED_Refresh();}

3. Communication between STM32 and FPGA

For the STM32 part, I also used the principle of SPI communication with the OLED screen, controlling the chip select via software, configuring MOSI and SCK in SPI2 to send data. The waveform information is divided into 6 bytes and sent sequentially. To align, the amplitude and waveform data are each set to one byte, while the frequency data is divided into 4 bytes.

printf("hello world!"); if(key1_num==4)      {                            // Send 6 bits of data at once          spi1_cs_set();// Amplitude data            HAL_SPI_Transmit(&hspi1, &amp, 1, 1000);      spi1_cs_res();
      spi1_cs_set();// Waveform      HAL_SPI_Transmit(&hspi1, &wave, 1, 1000);      spi1_cs_res();                                spi1_cs_set();// Frequency data 1      HAL_SPI_Transmit(&hspi1, &data1, 1, 1000);      spi1_cs_res();                                spi1_cs_set();// Frequency data 2      HAL_SPI_Transmit(&hspi1, &data2, 1, 1000);       spi1_cs_res();                                spi1_cs_set();// Frequency data 3      HAL_SPI_Transmit(&hspi1, &data3, 1, 1000);      spi1_cs_res();                                spi1_cs_set();// Frequency data 4      HAL_SPI_Transmit(&hspi1, &data4, 1, 1000);      spi1_cs_res();                                                                          key1_num=0;                        

The FPGA part references code from Eetreehttps://www.eetree.cn/wiki/spi_verilog, processing the received data. The SCK rate of the STM32 is adjusted to match the 12M clock frequency on the FPGA. Through testing, the data received by the FPGA is relatively stable when the SPI of the STM32 operates at approximately 1000kBits/s.

module stm_fpga_spi(clk, SCK, MOSI, SSEL,freq,ware, amp,LED1);input clk;input SCK, SSEL, MOSI;
output reg LED1;output reg [7:0] amp;output reg [7:0] ware;output reg [23:0]freq;  
// sync SCK to the FPGA clock using a 3-bits shift registerreg [2:0] SCKr;  always @(posedge clk) SCKr <= {SCKr[1:0], SCK};wire SCK_risingedge = (SCKr[2:1]==2'b01);  // now we can detect SCK rising edgeswire SCK_fallingedge = (SCKr[2:1]==2'b10);  // and falling edges
// same thing for SSELreg [2:0] SSELr;  always @(posedge clk) SSELr <= {SSELr[1:0], SSEL};wire SSEL_active = ~SSELr[1];  // SSEL is active lowwire SSEL_startmessage = (SSELr[2:1]==2'b10);  // message starts at falling edgewire SSEL_endmessage = (SSELr[2:1]==2'b01);  // message stops at rising edge
// and for MOSIreg [1:0] MOSIr;  always @(posedge clk) MOSIr <= {MOSIr[0], MOSI};wire MOSI_data = MOSIr[1];
// Receiving part// we handle SPI in 8-bits format, so we need a 3 bits counter to count the bits as they come inreg [2:0] bitcnt;
reg byte_received;  // high when a byte has been receivedreg [7:0] byte_data_received;always @(posedge clk)begin  if(~SSEL_active)    bitcnt <= 3'b000;  else  if(SCK_risingedge)// Sample on rising edge  begin    bitcnt <= bitcnt + 3'b001;
    // implement a shift-left register (since we receive the data MSB first)    byte_data_received <= {byte_data_received[6:0], MOSI_data};  endend
// Indicate that reception is completealways @(posedge clk)   byte_received <= SSEL_active && SCK_risingedge && (bitcnt==3'b111);  // Count the 6 groups of datareg [7:0] count;always @(posedge clk) begin  if(byte_received)begin    if(count>=5)begin       count<=0;                end    else    count<=count+8'h1;  // Count the messages    endend     reg [31:0] freq_temp; reg [7:0]ware_temp; reg [7:0]amp_temp; reg flag;// Divide into six states to receive different dataalways @(posedge clk)     if(byte_received) begin     case(count)       0:amp_temp= byte_data_received;         1:ware_temp= byte_data_received;       2:freq_temp[7:0]= byte_data_received;       3:freq_temp[15:8]= byte_data_received;       4:freq_temp[23:16]= byte_data_received;       5:begin freq_temp[31:24]= byte_data_received;end                                default:begin  ware_temp =0;                         amp_temp  =0;               freq_temp =0;                                           end        endcase end    // When count=5, combine the received dataalways @(posedge clk)     if(byte_received) begin      if(count==5)begin    freq<=freq_temp[7:0]+freq_temp[15:8]*100+freq_temp[23:16]*10000+freq_temp[31:24]*1000000;    ware<=ware_temp;    amp<=amp_temp;  end  end   endmodule

4. DDS Section

This part references materials on DDS from Eetreehttps://www.eetree.cn/wiki/dds_verilog and other resources. First, the clock frequency is increased to 120MHz using a PLL, directly utilizing the IP core provided by Radiant software. A 41-bit phase accumulator is used to achieve a precision of 0.0000546Hz. The frequency data received is multiplied by 18325 to achieve a precision of 1Hz, enabling adjustable frequency.

wire            clk_120M;pll_120M u_pll_120(        .ref_clk_i (clk),    .rst_n_i(rst_n),    .outcore_o(clk_120M),        .outglobal_o()        );     reg [40:0] phase_add;    always @(posedge clk_120M or negedge rst_n) begin        if(!rst_n) begin            phase_add <=0;        end        else            phase_add <= phase_add + freq_input_dds*18325;    end

Implementation of square and triangle waves:

assign  square_data = phase_add[40] ? 10'd1023:10'd0;  assign  triangle_data=phase_add[40] ? ~phase_add[39:30]:phase_add[39:30];  assign  DC=10'h3FF;

Implementation of sine waves primarily utilizes a lookup table. I generated waveform information using MATLAB, retaining one-quarter of the waveform data and using symmetry to map the entire waveform.

lookup_tables u_lookup_tables(.phase(phase_add[40:31]),.sin_out(sin_data));
assign sin_out = sine_onecycle_amp[9:0];assign sel = phase[9:8];sin_table u_sin_table(address,sine_table_out);
always @(sel or sine_table_out)begin  case(sel)  2'b00:   begin      sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];      address = phase[7:0];         end    2'b01:   begin      sine_onecycle_amp = 9'h1ff + sine_table_out[8:0];      address = ~phase[7:0];         end    2'b10:   begin      sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];      address = phase[7:0];         end    2'b11:   begin      sine_onecycle_amp = 9'h1ff - sine_table_out[8:0];      address = ~ phase[7:0];         end  endcaseend

Results and Observations

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

Swipe to see more

Resource Usage:

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

Areas for Improvement:

– The method for determining the rotation direction of the rotary encoder is relatively simple and crude, leading to some instability in actual input. Future attempts could explore timer-based encoder modes for decoding.

– The overall input of waveform information requires all information to be input before transmitting the signal to generate the waveform. Future attempts could allow for real-time input of various information.

– The final waveform effect is not particularly good, with some distortion occurring at high frequencies. Some filtering algorithms could be tried to improve precision.

Insights and Reflections

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform GeneratorCollaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

Overall, this project is relatively simple, but many issues arose during the process, and due to my limited skills, many parts of the code were written quite simply, resulting in many imperfections in the final outcome.

Through this activity, I feel that my greatest gain is the improvement of my problem-solving ability, finding relevant information online, and debugging the code step by step to identify where the problem lies and then making modifications. The communication between STM32 and FPGA was the last part I worked on. I initially thought it would be simple, but I unexpectedly got stuck for a long time. I didn’t know whether the problem was on the STM32 side or the FPGA side, and after a lot of debugging with the debug lights and simulation, I gradually narrowed down the range and finally achieved successful communication.

Based on the shortcomings of this project, I will continue to improve and learn more in the future.

Collaboration of STM32 and FPGA: Creating a High-Precision Local Control DDS Arbitrary Waveform Generator

Click “Read the original text” to view the project

Leave a Comment