Sine Wave Generator - Part 17 Microcontroller Basics (PIC10F200)


Note: Microchip recently made some changes and their newer IDE versions use the XC8 compiler for Assembly, whereas all sample code here is created for the MPASM compiler. However, the sample code has been ported to XC8 by user tinyelect on our Discord channel (to whom we are extremely grateful!) and the XC8 version of the code is at the bottom of this tutorial for your reference. To see the changes needed to switch from MPASM to XC8, please check out the process that he shared.

Hi again! This time we will rest after the two last huge tutorials and make a really simple device with a short and easy program. I suggest you make the sine wave generator using the PIC10F200 microcontroller. It will not control some external signal generator or other chip, it will produce the signal itself.

To produce a changing voltage, the microcontroller needs to have a digital-to-analog converter (DAC). We have already done this in the PWM tutorial, when we changed the brightness of the LED by changing the duty cycle of the pulses. When averaged, these pulses form the voltage of a different level from 0 (when duty cycle is 0%) to VCC (when duty cycle is 100%). But this method will not suit us this time. First, because the software PWM is quite slow, and, second, because the pulsing will be very high we would need a cumbersome filter.

So this time we will make a parallel DAC based on the so-called R-2R chain. This method allows us to produce a changing voltage signal with the resolution of the number of the microcontroller’s pins used in it. As long as PIC10F200 has just three outputs, the DAC will have 3 bit resolution, which means that it will be able to have just 23 = 8 voltage levels, but that’s OK for us, we will smooth the signal out using a simple RC-chain. Let’s now consider the schematic diagram of the device (figure 1).

Figure 1. Schematics of the sine wave generator.
Figure 1. Schematics of the sine wave generator.

As you can see, there are a lot of resistors whose purpose is not clear yet. Let’s figure this out.

Resistors R1-R6 form the above-mentioned R-2R chain. Why is it called this and what is its purpose? This is a chain which consists of resistors of two values R (in our case R=1kOhm), and 2R (2kOhm respectively). The 2R resistors (R1, R2, and R3) are connected in series to each output of the microcontroller, while R resistors (R4 and R5) are connected between the 2R resistors. The R6 resistor which is also 2R is mandatory and connects the last node to the ground. As you can see, there are three nodes (1, 2, and 3) where the resistors are connected. The output node is 1.

The feature of this chain is that it allows us to sum the input voltages with the weight coefficients. For instance, if we apply the high level only to the GP2 pin, then in node 1 there will be voltage of VCC/2. If we apply the high level only to the GP1 pin, then we will have VCC/4 in the node 1. And if we apply the high level only to the GP0 pin, the output voltage will be VCC/8. As I said, this chain allows us to sum the voltages, so the resulting output voltage will be:

Here VGPx is the voltage on the corresponding pin, it can be either 0 or VCC.

Thus we can form any voltage from VCC/8 to 7VCC/8 with the step of VCC/8 using this chain. If we had more outputs we could add more digits and increase the resolution by adding another R-2R pair.

The pins of the microcontroller are not selected randomly: the higher weight of the digit corresponds to the bigger GPIO number, thus we can just write the number from 7 to 0 into the GPIO register and get the corresponding output voltage of 7VCC/8 to 0.

As we have just 8 steps we need to smooth the output signal, and thus we use a simple low-pass filter for this, based on R7 and C1.

As usual, if you want to know more about the R-2R chain or low-pass filtering, feel free to search on the internet, and if not, just believe me that it works in this way. Trust me...

Now, as we have the means to produce the changing voltage, we need to form the sine signal somehow. The most obvious way is to get the time value T, then calculate sin(T), then convert it to fit the DAC range, and finally output it. But the problem here is that calculation of the sin(x) function can use all the resources of the microcontroller and take a lot of time. That’s why we will use another option. We will calculate the required values beforehand, in Excel for instance, then just copy the calculated values into the microcontroller and output them consecutively. This will be much easier.

So let’s do these calculations (Table 1).

Table 1. Calculations of the sine wave.
Table 1. Calculations of the sine wave.

Here N - sample number, T - time, sin(T) - value of the sine function, DAC - the value of the sin(T) converted into the DAC units, Register - number of the microcontroller’s register. Let’s consider Table 1 in more detail.

First, we have 16 samples, this number is limited by the volume of available RAM of the PIC10F200 microcontroller. Now, why do we have such weird values of T? They are obtained by dividing the sine period (as you know it’s 2𝜋, or approximately 6.28) by 16. So the next 17’th value will be exactly 2𝜋, and sin(2𝜋) = sin(0) = 0, and thus we can create the continuous sine wave by sending these 16 values in the endless loop.

The sin(T) column contains just the calculated values of the sine function, in the range of [-1, 1]. But these values don't suit us so we have to convert them so -1 of the sine will correspond to the 0 of the DAC (the smallest possible value), and 1 of the sine will correspond to the 7 of the DAC (the greatest possible value). To do this, we need to use the next equation:

where “round” is the operation of rounding the result to the nearest integer value. As you can see, the DAC values don't look like they can form the sine wave. Let’s see the graph of the DAC(T) dependence which Excel plotted for us (figure 2).

Figure 2. DAC(T) graph.
Figure 2. DAC(T) graph.

Well, this signal slightly looks like the sine wave, if you squint right. Also, Excel joins the points by straight lines, and in fact there will be just steps between them, so it’ll look even less like this. That’s why the low-pass filter is mandatory.

And the last column of Table 1 is the Register, it shows the register number in which we will write the corresponding value from the DAC column.

So the logic of the program will be the following: we will preload the registers with the DAC values, and then just send them in a loop to the outputs by copying these values into the GPIO register. As you probably remember from the tutorial about the code lock, we have the indirect addressing feature which will help us to make the program much simpler.

OK, let’s now consider the program code.

#include ""


ORG 0x0000


MOVLW  ~(1<<T0CS) ;Configure GP2 as GPIO


    CLRW    ;Clear the TRISGPIO register to configure

    TRIS GPIO   ;all GPIOs as outputs

;Preload the registers with the required values according to the sine table

    MOVLW 4    

    MOVWF 0x10

    MOVWF 0x18

    MOVLW 5

    MOVWF 0x11

    MOVWF 0x17

    MOVLW 6

    MOVWF 0x12

    MOVWF 0x16

    MOVLW 7

    MOVWF 0x13

    MOVWF 0x14

    MOVWF 0x15

    MOVLW 2

    MOVWF 0x19

    MOVWF 0x1F

    MOVLW 1

    MOVWF 0x1A

    MOVWF 0x1E

    CLRF 0x1B

    CLRF 0x1C

    CLRF 0x1D

    MOVLW 0x10   ;Copy the 0x10 register address to the

    MOVWF FSR   ;indirect address pointer

LOOP   ;Start of the main loop

    MOVF INDF, W   ;Read the value of the indirectly addressed

    MOVWF GPIO   ;register and copy it into the GPIO

    INCF FSR   ;Set the next address to read

    BTFSS FSR, 4   ;If bit 4 of the FSR register is not set

    BSF FSR, 4   ;then set it

    GOTO LOOP     ;loop forever


This program is really short in comparison to the previous ones. In the initialization part we configure GP2 as GPIO (lines 6-7), and then configure all GPIOs (except for GP3) as outputs (lines 8-9). In lines 12 to 33 we load the values into the file registers according to Table 1, there is nothing you need to be explained here by now, I hope.

Finally, we load the value 0x10 into the FSR register (lines 35, 36), which, as you hopefully remember, represents the address of the register in the indirect addressing. As we want to start sending the values from register 0x10, we load exactly this address.

The main loop of the program is even shorter than the initialization part (lines 38-44). And it’s quite straightforward.

LOOP   ;Start of the main loop

MOVF INDF, W   ;Read the value of the indirectly addressed

    MOVWF GPIO    ;register and copy it into the GPIO

    INCF FSR   ;Set the next address to read

    BTFSS FSR, 4     ;If bit 4 of the FSR register is not set

    BSF FSR, 4    ;then set it

    GOTO LOOP      ;loop forever

First, we copy the value of the INDF register into the W register (line 39). I’ll remind you that the INDF register is the one whose address is written into the FSR register. So at the first iteration the INDF has the value of the 0x10 register which is 4 (as follows from Table 1 and lines 12, 13).

Then we copy this value into the GPIO register (line 40), after which the GP2 will be set high, and GP1 and GP0 will be set low, and the output voltage will be:

Then we increment the FSR register value (line 41), so it will point to the 0x11 register the next time. The FSR register has active only lower 5 bits, and the upper 3 bits (7 to 5) are not used. So when we increment it several times and reach the value of 0x1F which is the last register address, on the next iteration we will have the value 0x20, or 0b00100000 in it. As long as we can ignore the 1 in bit 5, we still should care about lower 5 bits. Because now the FSR register will point at register 0x00 which we don’t need. So, we have to check bit 4 of the FSR register, and if it becomes 0, this means that we moved from the value 0x1F to the value 0x00, we need to set the bit 4 and thus make the FSR register 0x10 which is the first address of the file register where we have the first DAC value. We implement this checking in lines 43, 44.

And that’s all! Quite simple, huh?

Let’s now assemble the schematics and get the results. This time you will need an oscilloscope. You can also connect to a buzzer and listen to the sound but this will not give you the full picture. If you don’t have an oscilloscope, I’ll show you what happens here.

In figure 3, you can see the oscillogram of the output signal without the low-pass filter (when C1 is removed). As I said, it’s a total mess with huge steps, and doesn’t look like sine wave (maybe very remotely).

Figure 3. Output signal without the filtering.
Figure 3. Output signal without the filtering.

In figure 4, there is the output signal with C1 = 3.3nF. As you can see, the signal is now relatively good, but you can notice some fractures near the points.

Figure 4. Output signal with C1 = 3.3nF.
Figure 4. Output signal with C1 = 3.3nF.

In figure 5, there is an output signal with C1 = 6.8nF. Here the signal looks good already, and the fractures are barely noticeable.

Figure 5. Output signal with C1 = 6.8nF.
Figure 5. Output signal with C1 = 6.8nF.

In figure 6, there is an output signal with C1 = 10nF. In this case it is very smooth.

Figure 6. Output signal with C1 = 10nF.
Figure 6. Output signal with C1 = 10nF.

But if you look at figures 4-6, you may notice that the amplitude of the signal reduces (the Vpp parameter in figures), and it will reduce more if you increase the capacitance of the C1 as it will start to filter the useful signal instead of smoothing out the steps, so 6.8 nF is a good compromise between the smoothing and the amplitude.

Also, from figure 3 to figure 6, you can see that the frequency of the sine wave is about 7.89kHz (FRQ parameter), and this is the maximum frequency we can reach in our program, as you see, there are no delays there. You can reduce it, though, by inserting additional delays with NOP or GOTO $+1 instructions.

Also, I want to note that the load capacity of such a schematic is not very high, so if the load resistance is comparable with R (1kOhm in this case), it can significantly influence the output voltage. In this case you need to put some sort of amplifier at the output. The simplest solution is the emitter follower which requires just one transistor. A more advanced one is an op amp connected as a voltage follower. But make sure that your op amp has the rail-to-rail feature, otherwise it can cut out the top and bottom of the signal.

As a conclusion, I want to say that we’ve learned how the parallel DAC based on the R-2R circuit works. This circuit allows us to use the microcontroller without analog feature support as one that can output an analog signal. The code size of the sine wave generator is just 34 words.

As homework, I suggest you try to change the parameters of the program and the schematic and see how they influence the signal form: the frequency of the signal, the low-pass filter values (R7 and C1). Also, I want to suggest you to write other values into the registers to build signals with other forms, like saw, triangle, or trapezoidal waves.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?