This project was our Laboratory #9 for our course BCA143: Bare Metal Programming. In this laboratory project, our goal is to make a bare metal RT Spark 1 STM32F407XX Program that utilizes the DAC Component on PA4 and using DMA (Direct Memory Access) to the DAC. We are required to generate SQUARE, RAMP and SINE signals to be observed in the oscilloscope.
How it WorksThe project works by utilizing the internal built in DAC (Digital to Analog Converter) feature of the STM32F407XX Microcontroller. It is shown in the Microcontroller block diagram from STM32F407XX's Datasheet below
Also, we will be using DMA1 (Direct Memory Access) for DAC1 to send data. As you can see on the block diagram below, DMA1 is physically connected to the AHB/APB1 (Advanced Peripheral Bus 1) which DAC1 is currently connected in.
We are also using a timer component which is TIM6 also highlighted in the block diagram below. It is also wired directly to the same bus as the DAC as you can see below. You can trace the bus line and see that TIM6 and DAC1 are connected together. TIM8 is also just a basic timer. So that we do not need other additional features to configure. This timer just serves us by triggering the DAC through the TRGO trigger output.
With the 3 of these components working together, we can now generate signals with our DAC component in the MCU.
The project will then use the C program code to output DAC voltages in various forms of SQUARE, SINE and RAMP waves.
The whole system process flow that this project utilizes is shown in the figure below
The flow shows that after Power on or Reset, it initializes the HAL, System Clock, GPIO, DMA, DAC and TIM6 configurations. Now, in our USER CODE part, we started TIM6 Timer and then call tone_play(WAVEFORM) where WAVEFORM will be the enum type we put later for SQUARE, RAMP and SINE. Inside tone_play, it will then reset the DAC by stopping all requests and creating a new request configuration depending on the waveform that we want.
switch(wave_type) {
case SQUARE:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)square_wave_lut, LUT_SIZE, DAC_ALIGN_8B_R);
break;
case RAMP:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)ramp_wave_lut, LUT_SIZE, DAC_ALIGN_8B_R);
break;
case SINE:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave_lut, LUT_SIZE, DAC_ALIGN_8B_R);
break;
}As you can see on the code above, we're using switch statements to check and initialize the correct LUT for the waveform we want. The LUT is just a Lookup Table array for the values needed to be sent to the DAC to trigger the correct waveform.
After that, our code steps into the while(1) loop. However, we do nothing in the loop since our code will be triggered by TIM6 to generate the DAC output waveforms.
In the figure shown below, The process flow of the TIM6 timer is defined. It first checks the ARR if it is already greater than 9 or equals to 9 (the counter starts counting from 0), then we send TRGO Pulse signal to DAC. If not, it just increments the ARR counter until it reaches the threshold to trigger once again.
Once the TRGO pulse has been sent, it will read 1 byte from the Waveform LUT array and then write this to the DAC Register via DMA Controller to output it. Then it will reset the ARR counter since the timer triggered the execution due to the condition that ARR is >= 9 already.
Therefore when the DAC Register is written, it will then output this to our DAC pin which is PA4. This pin will be then monitored using an oscilloscope which will be shown later after we setup everything.
Software SetupMCU Configuration Setup
To setup our Software requirements for this project, we will be utilizing STM32CubeMX for generating the boilerplate MCU Configurations that we can then use in the STM32CubeIDE for programming our RT Spark 1 development board.
To start, we will first need to open STM32CubeMX and create a new project.
Once opened, you should see your screen display the same as shown in the figure below
Go to File > New Project to Create a New Project
After that, a window should pop-up. This is the Project Creation Screen.
We will need to use RT Spark 1's Microcontroller Part Number which will be STM32F407ZGT6. Use that to search for the exact MCU in the STM32CubeMX Program and select it.
Once you have selected the exact MCU Part Number, click on Start Project button on the top right corner of the New Project Window
Then you should see the screen below. This indicates that the STM32CubeMX Project has been successfully created.
GPIO Configuration
We will first start by configuring the GPIOs we will need to utilize in this Project.
The following list are the GPIO Pins we will be utilizing for this Project.
PA4-DAC_OUT1Mode, The pin that we will hook up one channel of our oscilloscope. This is the output of DAC1 which outputs the generated waveforms.
Since this is a very basic more software focused project, we will only utilize 1 GPIO pin for now.
We start by Configuring PA4 as an Output Pin of DAC_OUT1 mode. Click pin PA4 or search it by typing on the bottom part of the screen.
Once set with the correct mode for the DAC, we can then proceed to check the global GPIO settings by going to System Core > GPIO
As you can see, PA4 is set to Analog output mode. And then the signal pin is DAC_OUT1. This is all what we need for this project.
Now, let's configure other things in STM32CubeMX.
Let's start with the Clock Config. We will need our clock to be running at maximum speed possible for better smooth generated waveforms that we can observe later. To do this, let's go to Clock Configuration tab.
Once in this tab, find the HCLK input box and type 168 on it. This sets our MCU to run at its max speed of 168Mhz.
Once you entered 168 in the box, press the enter key in your keyboard and it should prompt the message shown in the figure below
It will ask to use other sources. This is because the clock configurations need to be dependent on each other. Changing the HCLK requires adjusting the frequencies of the other devices too. To automatically do this, just press enter or click OK in the confirmation window that will appear, and it will auto find the solutions for the clock configurations.
Once done, we will now proceed to configuring our DAC Peripheral. To do this, we go to Analog > DAC as shown in the figure below.
Make sure to check the OUT1 Configuration checkbox to enable DAC1. Then on the bottom part of the window, check out the Parameter Settings. Make sure to set them exactly as shown in the figure above
- Output Buffer - Enable
- Trigger - Timer 6 Trigger Out event
- Wave generation mode - Disabled
It should be configured as shown above to make sure the DAC works.
After this, we will now configure our DMA (Direct Memory Access) to write to the DAC Peripheral. To do this, we go to the bottom part of the screen and switch to the DMA Settings tab to configure the DMA.
When you first see this menu, the DAC list under DMA Settings will be completely empty. Click on the Add button and select DAC1. Then set the configuration to the following:
- Direction: Memory To Peripheral.Mode: Circular (This makes the wave repeat forever automatically).Data Width: Set both Peripheral and Memory to Byte (since our lab manual specifies the DAC uses 8 bits of resolution instead of the 12bits).
- Increment Address:We check "Memory" only (so it reads through our array), and we leave "Peripheral" unchecked (so it always writes to the single DAC register).
Once DAC1 and DMA has been configured correctly, we will now proceed to the Timer 6 configuration for this project.
To configure the timer, we must go to Timers > TIM6 as shown in the figure below.
First, check the Activated checkbox on the top. This enables the Timer 6 Peripheral. Once enabled, check the bottom part of the window in Parameter Settings.
We must make sure that the Timer 6 must be configured as shown in the figure above.
In Parameter Settings, we need to set the speed. Let's aim to update the DAC 100, 000 times per second which would be 100kHz in frequency. Since APB1 is 84MHz, we use the following configuration:
- Prescaler:
84 - 1(This steps the clock down to 1MHz. - Counter Period (AutoReload Register):
10 - 1(This triggers an event every 10 ticks, giving us exactly 100kHz. - Trigger Event Selection TRGO: Select Update Event (This is the actual pulse sent to the DAC).
Now after that, we go to the Project Manager tab as shown below.
We will name our project and set the directory. After that, we must set the Toolchain / IDE to STM32CubeIDE since we are going to use STM32CubeIDE to code our Program.Click on Generate Code button on the top right corner of the window to generate the project on the specified directory.
Once the project has been generated, it will show a Project Generated Successfully message. When this appears, we can now proceed to the next steps.
Now, since we are done with the STM32CubeMX configuration part, We can now proceed to our Program Code.
Program Code Setup
Now we open our STM32CubeMX generated project into STM32CubeIDE. To do this, open the IDE and then go to File > Open Projects from File System...
Now it should show a new Window for Importing the project directory.
Make sure to select the folder where STM32CubeMX generated the project. Then tick the checkbox on the list that will appear in the Import Window, then click Finish. It should now import the project in the IDE.
Now, after importing the project, we expand the Project Name in the Project Explorer pane and then open the main.c file under Core > Src > main.c
Once main.c is opened, we are now ready in putting the code that will make our Project work.
Now we start at the enums and defines. First, find BEGIN USER CODE PTD code comment in main.c and then we will put the following code inside:
/* USER CODE BEGIN PTD */
typedef enum {
SQUARE = 0,
RAMP,
SINE
} WaveType_t;
#define LUT_SIZE 100
/* USER CODE END PTD */Now we have defined our waveform types using enum and then the Lookup Table size of 100. We will now proceed to the next part which is the USER CODE BEGIN PV. Find the USER CODE BEGIN PV line and inside it, we use the following code:
/* USER CODE BEGIN PV */
// Square Wave Pre Calculated values for high and low waves
uint8_t square_wave_lut[LUT_SIZE] = {
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
// Ramp Wave Linear step from 0 to 255
uint8_t ramp_wave_lut[LUT_SIZE] = {
0, 2, 5, 7, 10, 12, 15, 17, 20, 22, 25, 28, 30, 33, 35, 38, 40, 43, 45, 48,
51, 53, 56, 58, 61, 63, 66, 68, 71, 73, 76, 79, 81, 84, 86, 89, 91, 94, 96, 99,
102, 104, 107, 109, 112, 114, 117, 119, 122, 124, 127, 130, 132, 135, 137, 140, 142, 145, 147, 150,
153, 155, 158, 160, 163, 165, 168, 170, 173, 175, 178, 181, 183, 186, 188, 191, 193, 196, 198, 201,
204, 206, 209, 211, 214, 216, 219, 221, 224, 226, 229, 232, 234, 237, 239, 242, 244, 247, 249, 252
};
// Sine Wave: Pre calculated values for a perfect circle
uint8_t sine_wave_lut[LUT_SIZE] = {
127, 135, 143, 151, 158, 166, 174, 181, 188, 195, 201, 207, 213, 219, 224, 229, 234, 238, 241, 245,
248, 250, 252, 254, 254, 255, 254, 254, 252, 250, 248, 245, 241, 238, 234, 229, 224, 219, 213, 207,
201, 195, 188, 181, 174, 166, 158, 151, 143, 135, 127, 119, 111, 103, 96, 88, 80, 73, 66, 59,
53, 47, 41, 35, 30, 25, 20, 16, 13, 9, 6, 4, 2, 0, 0, 0, 0, 0, 2, 4,
6, 9, 13, 16, 20, 25, 30, 35, 41, 47, 53, 59, 66, 73, 80, 88, 96, 103, 111, 119
};
/* USER CODE END PV */Now we have defined the Lookup Tables for Square, Ramp & Sine waves. Next, we proceed to the next part which is the function definitions in our USER CODE BEGIN 0. We will define our tone_play() function here. To do this, find USER CODE BEGIN 0 section and put the following code:
/* USER CODE BEGIN 0 */
void tone_play(WaveType_t wave_type) {
// Stop any currently running DMA request
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1);
// Start the new DMA request depending on the parameter passed
// We use DAC_ALIGN_8B_R to inform the MCU we are sending 8-bit resolution data.
switch(wave_type) {
case SQUARE:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)square_wave_lut, LUT_SIZE, DAC_ALIGN_8B_R);
break;
case RAMP:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)ramp_wave_lut, LUT_SIZE, DAC_ALIGN_8B_R);
break;
case SINE:
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave_lut, LUT_SIZE, DAC_ALIGN_8B_R);
break;
}
}
/* USER CODE END 0 */After we have setup our main codes, we will now setup the last part of the code which will be the USER CODE BEGIN 2 which is inside our main function.
To do this, find the USER CODE BEGIN 2 section and put the following code inside:
/* USER CODE BEGIN 2 */
// Start the Timer
HAL_TIM_Base_Start(&htim6);
// Play the desired signal
tone_play(RAMP);
/* USER CODE END 2 */In our USER CODE BEGIN 2, we have set the tone_play() function to generate the RAMP waveform by default. We can see this later when we test it.
Since we are done with our code, we can now build and flash our code into the RT Spark 1 Development board. To do this, we need to do additional steps.
First, right click on the project name in the Project explorer and choose Run As > 1 STM32 C/C++ Application
Now, a window will appear for the Run As configuration. Just do not change anything and directly click the OK button.
The configuration is already automatically set. We just need to configure the project as an STM32 Application so that we can flash this onto our RT Spark 1 development board.
After we configured Run As settings, we can now Build and Flash the code onto the board by going onto the toolbar and pressing the Hammer Icon.
Once the build is successful and has no errors, It should say Build Finished. After the build has finished, click on the Arrow Down icon next to the Play Icon on the toolbar to select the STM32 Application to be flashed on the board. Make sure the RT Spark 1 is connected via its USB C cable.
After the code has been flashed, congratulations! We can now proceed to the testing and visualize the generated waves with an oscilloscope.
TestingTo test and visualize our generated waveforms, we first need to connect a channel of our oscilloscope into our DAC_OUT1 pin which is PA4. Also do not forget to clip the black aligator clip of the probe to any metal points in the RT Spark 1 development board or the GND pin to ground the probe. The Probe will need GND reference which will be the 0v level of the RT Spark 1 board.
In my case, CH2 is the channel I connected to PA4, and then the GND black aligator clip to a jumper wire connected to the GND pin of the RT Spark 1. When starting the RT Spark 1, we should see the RAMP Wave.
As you can see on the video above, the Ramp Wave has been perfectly shown in the oscilloscope. I have adjusted the volts/div and the time knobs in order to precisely see the RAMP waves that our DAC is generating.
Now, we will try the next waveform which will be SQUARE. To do this, we just change the enum that we passed in our tone_play() function.
As you can see on the figure above, we changed the enum passed on the tone_play() function param from RAMP to SQUARE. Next is to rebuild the project and reflash it again to the RT Spark 1 development board.
We should be able to see the square wave being generated by our DAC with DAC_OUT1 as the same pin which is PA4.
As you can see on the video above, the SQUARE wave has been successfully read by the oscilloscope on the same PA4DAC_OUT1 pin. This means that our code for both the SQUARE and RAMP waves are working.
Now, we proceed to the last waveform we have in the code which is SINE. To set the DAC output as a sine wave, we need to edit the tone_play() function parameter once again. We will change it from SQUARE to SINE.
As you can see on the figure above, we have changed the enum that is passed onto the tone_play() function. Now it is changed to SINE. To see the results, we just rebuild the project once again and then reflash it to the development board. Now, we should be able to see the oscilloscope output a sine wave.
As you can see on the video above, we have successfully generated the SINE waveform.
This means that our DAC Waveform generator code is fully working without issues. We can now proceed to analyze what these oscilloscope readings mean based also on our professor's questions in our laboratory guide.
Analysis & InterpretationSquare Wave
1. What is the minimum output voltage?
Answer: The minimum output voltage of the DAC for the square wave is 0v. If you refer on Figure 31 above, you can see Va is -52.8mV. This is our voltage minimum.
But why is it 0v and not -52.8mV? This is because the values shown in the oscilloscope voltage cursor is based on the offset position of the graph in the display. This means that we need to actually offset the graph in the very bottom of the display to make Vb 0v. Also, RT Spark 1 or STM32F407XX does not have the capability to output negativ voltages.
2. What is the maximum output voltage?
Answer: The maximum output voltage of the DAC for the square wave is 3.30v. If you refer on Figure 31 above, you can see Vb is 3.25v. This is not our voltage maximum. Since our graph has an offset, we must subtract Vb with Va, hence our ΔV of 3.30v. The maximum output voltage also aligns within the voltage range that STM32F407XX can output safely.
3. What is the rise time of the output signal?
Answer: The rise time of the square wave is approximately 496.0μs, which is around 496000ns when converted. If you refer from Figure 32 above, you can see ΔT reading is 496.0μs. The blue vertical lines are the time cursor lines which are placed between the rising and falling edge of the signal. Hence, measuring the rise time of the square signal. This value is also approximate, as oscilloscope reading tends to vary every time.
4. What is the fall time of the output signal?
Answer: The fall time of the square wave is approximately 504.0μs, which is around 504000ns when converted. If you refer from Figure 33 above, you can see ΔT reading is 504.0μs. The blue vertical lines are the time cursor lines which are placed between the falling and rising edge of the signal. Hence, measuring the fall time of the square signal. This value is also approximate, as oscilloscope reading tends to vary every time.
5. What is the period of the output signal?
Answer: The period of the output signal is exactly 1000.0 μs or 1ms when converted. This was calculated by adding the measured High Time from the answer in Question #3 above which is 496.0 μs and Low Time from the answer in Question #4 above which is 504.0 μs. A 1 ms period confirms that the STM32F407XX MCU is generating exactly a 1 kHz frequency, which perfectly matches our software design.
Ramp Wave
1. Why is the rising edge of the ramp wave not smooth? The DAC has 8 bits of resolution, but the resulting waveform is jagged.
Answer: Even though the DAC is operating at an 8 bit resolution capable of 256 distinct voltage levels, the wave is jagged because a microcontroller cannot output a continuous, sloped line. It can only output flat, discrete steps.
In our project code, we defined an array (LUT_SIZE) of only 100 data points to draw the entire wave. Because Timer 6 ticks at 100 kHz, the DAC grabs a number from the array, sets the physical voltage, and holds it there completely flat for exactly 10 microseconds before moving to the next number. Furthermore, to reach 255 in only 100 steps, the array skips numbers (0, 2, 5, 7, 10...). The jaggedness we see on the oscilloscope is the visual representation of the DAC physically holding these skipped, discrete voltage levels over time.
2. How can you smooth the rising edge in hardware?
Answer: We can smooth the jagged edges in hardware by adding an analog RC Low-Pass Filter to the DAC_OUT1 pin PA4. By placing a simple resistor in series with the signal and a capacitor tied to ground, the capacitor acts like a small, fast response battery. Instead of the voltage snapping instantly upward in sharp, 90 degree stair steps, the capacitor absorbs and releases the energy slowly. This physical charge/discharge cycle averages out the sharp jumps, transforming the jagged staircase into a smooth, continuous slope.
3. How can you smooth the rising edge in software?
Answer: We can smooth the wave in software by increasing both the Sampling Rate and the Bit Resolution.
- Increase Data Points (Sampling Rate): Instead of an array of 100 points, we define an array of 1, 000 points and speed up Timer 6 to 1 MHz. This makes the horizontal width of each step microscopically short.
- Increase Bit Depth (Resolution): The STM32F407XX DAC hardware natively supports up to 12 bit resolution. By changing our software configuration from
DAC_ALIGN_8B_RtoDAC_ALIGN_12B_R, the MCU divides the 3.3V into 4, 096 vertical steps instead of just 256. This makes the vertical height of each step practically invisible to the eye.
Sine Wave
1. Experiment with the code to determine the maximum frequency sine wave which can be generated.
Answer: The maximum clean sine wave frequency generated is approximately 10 kHz to 20 kHz assuming we keep using the 100 point array.
By decreasing the TIM6 Auto Reload Register (ARR) to speed up the timer, the maximum clean sine wave achieved was approximately 10 kHz. Beyond this limit, the waveform deformed into a jagged triangle and suffered amplitude collapse. This physical limit occurs because the STM32F407XX DAC has a maximum hardware conversion rate of 1 to 2 Mega samples per second (MSPS). Generating 100 data points per cycle any faster exceeds the DAC's physical settling time.
Project ConclusionsThis laboratory project successfully demonstrated the synthesis of continuous analog signals using a bare metal architecture. By utilizing the STM32F407XX’s Hardware Timer (TIM6), Direct Memory Access (DMA), and Digital to Analog Converter (DAC), precise Square, Ramp, and Sine waves were generated perfectly. Furthermore, analyzing these waveforms via an oscilloscope provided practical insights into digital to analog constraints, such as the staircase effect caused by discrete sample rates and the physical frequency limits dictated by the DAC's 1 MSPS maximum conversion rate.
This project demonstrated and bridged the gap between digital software arrays and real world analog hardware limitations.




Comments