CH32V003F4P6 - USART basics

In this article, I am going to continue the tutorial series that I started by introducing the CH32V003F4P6 microcontroller and some basic GPIO manipulation. This article’s topic is some basic USART. I chose to continue the tutorial series with USART because I already touched on the topic a little in the previous article, and it will be useful in the upcoming projects. For example, later on, we can send commands to the microcontroller to start or stop reading the ADC, we can receive data from sensors on the serial port of the computer, or we can send data to the microcontroller and print it on a display. So, in this article, I lay down the foundations of these features.

Coding

I will skip the introduction because I already covered it in the previous article. All I mention here is that I created a new project for the CH32V003F4P6 microcontroller and opened the main.c file. Since the code already contains the code for the USART, it is a great starting point. This USART code is placed in the while(1) loop and it simply grabs the incoming data and then sends back the inverted value of it.

The first example is based on this already included code snippet. But instead of sending back the inverted character, I send the same character back. I also moved all the code into a separate function and modified it in a way that it does not block the while(). The original code waits for the RXNE flag to change, so one cannot do anything else while the code is waiting in its own while loop (the while loop that waits for the flag to change).

 

Echoing back characters

So, this echoCharacterUSART() function is placed in the while(1) in the main code. The first if() condition checks the USART flag. More specifically, on USART1, it checks the USART_FLAG_RXNE flag. The condition becomes true if the flag is SET (!= RESET). Then the receivedData variable (char, declared as a global variable) gets the return value of the USART_ReceiveData() function. Then, the code immediately sends back the content of the receivedData variable. Finally, the code checks if the data was transmitted by calling the USARTGetFlagStatus() function and checking the status of the USART_FLAG_TXE’s value.

Now, if we connect to the microcontroller with CoolTerm and start sending characters to it, we will see that the code works just fine. Each character is returned as they should be returned. We can even send multiple characters at the same time and all of them will be returned.

However, there’s a catch! The returned text is not a string, but 3 individual characters printed after each other. The echoCharacterUSART() function was performed three times to send back the 3 characters to us.

 
void echoCharacterUSART()
{
    if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET)
    {
        receivedData = (USART_ReceiveData(USART1));
        USART_SendData(USART1, receivedData);
    }

    if(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != RESET)
    {
       
    }
}
 

Toggle LED with serial command

First, let’s make use of the fact that now we know how we can send characters to the CH32. Let’s combine it with the previous lesson where we learned how to turn on and off different GPIO pins. What we will do is we will toggle 4 LEDs with their assigned numbers 1, 2, 3 and 4. But let me re-emphasize it again that even though we send something seemingly a number, when the microcontroller looks at it, it is a character (char)!

We already have a global variable, receivedData in the code, so let’s write another function that checks the value of this variable, and based on the value, it toggles a specific LED. But first, we should define 4 GPIO pins as outputs. I reused the code for this from the previous lesson. Maybe one thing worth mentioning: You might notice that instead of defining all 4 pins on port D, the GPIO_Pin_1 is on port C. This is because PD1 is also the SWIO pin which we need for the debugging. So, I had to change this.

The function is placed in the main code in while(1). As you can see, I wrote a switch-case statement to check which pin should be toggled. This is a more elegant way of coding instead of writing a bunch of if()s. The code is simple. If any of the four numbers are sent to the CH32, the corresponding LED is toggled. First, the LED turns on, then when the same number is sent to the MCU again, the LED turns off. The code slightly blocks, but only when it must toggle a pin. I noticed that if I don’t add a little delay, “over-toggling” can occur and the LED does not change state properly.

 
void toggleLEDUSART()
{
    switch(receivedData)
    {
        case '1':
            GPIO_WriteBit(GPIOD, GPIO_Pin_0, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_0) == Bit_SET) ? Bit_RESET : Bit_SET);
            Delay_Ms(10);
            break;
        case '2':
            GPIO_WriteBit(GPIOC, GPIO_Pin_1, (GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_1) == Bit_SET) ? Bit_RESET : Bit_SET);
            Delay_Ms(10);
            break;
        case '3':
            GPIO_WriteBit(GPIOD, GPIO_Pin_2, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_2) == Bit_SET) ? Bit_RESET : Bit_SET);
            Delay_Ms(10);
            break;
        case '4':
            GPIO_WriteBit(GPIOD, GPIO_Pin_3, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_3) == Bit_SET) ? Bit_RESET : Bit_SET);
            Delay_Ms(10);
        break;
    }
}

What’s cool about this code snipped is that it can be customized freely. You do not need to toggle a GPIO pin when you receive a certain character from the computer. Instead, why not start an AD conversion? Or, why not store the subsequent value(s) being sent to the serial terminal? The possibilities are endless'!

 

Showing manipulated GPIO pins

Now let’s do the opposite of the above code. Let’s print a text when a button is pressed. Sure enough, we need to define 4 pins as inputs. 4, because I have 4 buttons. Same as in the previous example. In fact, I almost did the same thing in the previous example, but instead of sending the “ID number” of the pressed button, I increased or decreased the value of a variable and then printed the value on the serial terminal.

Here, as you can see, I just send a predefined text using the printf() function. I also added a linebreak (\n) so the next message is printed in the next line. On my breadboard, I wired the buttons according to the order seen in the code, so if I press the first button, then the button connected to PC0 is pressed and the corresponding message is sent to the PC.

Using the printf() function allows us a larger flexibility when it comes to sending text from the CH32 to the computer. We can send a whole text with it, we can even insert variables in the text and so on.

 
void sendButtonNumberUSART()
{
    if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_0) == Bit_RESET) //If PC0 goes LOW, we enter this part of the code
    {
        printf("Button 1 was pressed\n");
    }

   if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_2) == Bit_RESET) //If PC2 goes LOW, we enter this part of the code
   {
       printf("Button 2 was pressed\n");
   }

   if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_3) == Bit_RESET) //If PC3 goes LOW, we enter this part of the code
   {
       printf("Button 3 was pressed\n");
   }

   if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_4) == Bit_RESET) //If PC4 goes LOW, we enter this part of the code
   {
       printf("Button 4 was pressed\n");
   }
   Delay_Ms(100); 
}
 

Sending more complex commands

Now we advance towards more complex commands.

In this example, I want to turn on the LEDs individually in the same way that I did before using the 1, 2, 3 and 4 numbers, but this time, I want to add a “command character” in front of the numbers. This is useful when you have multiple similar items with the same numbering and you want to distinguish them. A good example is working with registers. For example, you both want to read and write a register. Then, for instance, R1, R2, R3…etc. commands can be used to read registers 1, 2 and 3…etc., and W1, W2, W3…etc. commands can be used to write registers 1, 2 and 3…etc.

What we do here is that we declare an index that will navigate the code through the commandBuffer array during the iteration in the while(). We also declare a receivedCharacter char variable that stores the actual character. Then, using the memset() function, we “erase” the commandBuffer’s whole length. Technically we set its first 10 items as zero.

Then in the while() we iterate through the (length-1) length until we finish the iteration or receive an endline character. Each iteration waits until a character arrives (remember the RXNE flag!), and then passes the received character to the corresponding variable. If the character was an endline character, the while() is aborted, the code ends here. Otherwise, the code proceeds and the received character is passed to the buffer. When the code exits the while() we add a null-terminate character to the end of the buffer.

 
void receiveCommandUSART(char *commandBuffer)
{
    uint8_t index = 0;
    char receivedCharacter;
    memset(commandBuffer, 0, 10);

    while (index < (10 - 1))
    {
        while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
        receivedCharacter = USART_ReceiveData(USART1); 

        if (receivedCharacter == '\n' || receivedCharacter == '\r') 
        {
            break;
        }
        commandBuffer[index++] = receivedCharacter;
    }
    commandBuffer[index] = '\0';
}
 

And now, we need to process the previously assembled command. The processCommandUSART() function receives the commandBuffer as a parameter (more precisely, its address) and then works with the data inside it.

Actually, it is fairly simple. The if() checks if the first item in the array is the character ‘L’. It also checks if the length is at least two. This implies that the array also contains the command number.

The code proceeds into the if() and first, it passes the number of the LED we want to trigger into a freshly declared variable. This is still just a character. We do something sneaky here. The characters are represented using their ASCII values. ‘0’ has an ASCII value of 48, ‘1’ has an ASCII value of 49, ‘2’ has an ASCII value of 50 and so on. So when we subtract ‘0’ from ‘49’, we get 50-48 = 2 which happens to be the actual numerical value of ‘2’. So, I applied this idea when the ledIndex is determined.

Finally, another if() checks if we are trying to trigger valid LEDs and then based on the index, one of the LEDs is toggled. “L1” turns on the first LED, the LED on PD0, “L2” turns on the second LED on PC1…and so on.

 
void processCommandUSART(const char *command)
{
    if (command[0] == 'L' && strlen(command) >= 2)
    {
        char commandNumber= command[1];  
        int commandIndex= commandNumber-'0'; 

        if (commandIndex>= 1 && commandIndex<= 4) 
        {
            switch (commandIndex) {
                case 1:
                    GPIO_WriteBit(GPIOD, GPIO_Pin_0, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_0) == Bit_SET) ? Bit_RESET : Bit_SET);
                    break;
                case 2:
                    GPIO_WriteBit(GPIOC, GPIO_Pin_1, (GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_1) == Bit_SET) ? Bit_RESET : Bit_SET);
                    break;
                case 3:
                    GPIO_WriteBit(GPIOD, GPIO_Pin_2, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_2) == Bit_SET) ? Bit_RESET : Bit_SET);
                    break;
                case 4:
                    GPIO_WriteBit(GPIOD, GPIO_Pin_3, (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_3) == Bit_SET) ? Bit_RESET : Bit_SET);
                    break;
            }
        }
    }
}
 

Okay, one last code snippet, I promise.

We looked at writing a certain specific GPIO using a command. Now let me do the same, but this time, I read a specific GPIO pin. This is good if I want to see if a certain pin changed its status. Typically, we would want to do this immediately by polling the pin or using interrupts, but sometimes we might want to check which button is pressed and we don’t care about time.

The strategy is the same, I almost exactly copied the previous code snippet. Now I use ‘B’ as the command character. One of the differences is that I added a new variable, pinStatus. This stores the status of the pin which could be HIGH (SET) or LOW (RESET) in an 8-bit integer variable. Then the switch() checks the selected pin and passes the result of the reading into the pinStatus variable. After the switch(), the code checks the value of the pinStatus variable and based on the value, it prints the corresponding message.

CH32V003F4P6 Dev board // Clicking the image opens my affiliate link!

 
if (command[0] == 'B' && strlen(command) >= 2) 
    {
            char commandNumber = command[1]; 
            int commandIndex = commandNumber - '0'; 
            uint8_t pinStatus = 0; 

            if (commandIndex >= 1 && commandIndex <= 4) 
            {
                switch (commandIndex)
                {
                case 1:
                    pinStatus = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_0);
                    break;
                case 2:
                    pinStatus = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_2);
                    break;
                case 3:
                    pinStatus = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_3);
                    break;
                case 4:
                    pinStatus = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_4);
                    break;
                }

                if (pinStatus == Bit_SET)
                {
                       printf("Pin %d is HIGH\n", commandIndex);
                }
                else
                {
                       printf("Pin %d is LOW\n", commandIndex);
                }
            }
     }

If you found this content useful, please consider joining my YouTube channel’s membership or leaving a donation.

Also, please consider using my affiliate links when buying relevant gadgets.

Previous
Previous

CH32V003F4P6 - Timers and PWM

Next
Next

CH32V003F4P6 - Setup and blinky, introduction to GPIOs