In our previous episode, we spent an unhealthy amount of time in front of a computer figuring-out some arcane USB code. Today, we finish what we started and finally get our STM32 to talk with a USB host. It’s time to integrate !
Just one thing before we start : I’ve seized the opportunity to turn this page into a tutorial on how to clone and integrate my STM32 libraries into your projects. As a result, it’s rather detailed and might cover a lot of things you already know. You may have reached this page looking for information on how to install a different library, so I’ve kept things mostly about Git and STM32CubeIDE.
1. Nefastor’s Easy-VCP Library
You’ve seen in the previous page how to configure an STM32CubeIDE project to setup the USB hardware and middleware. Obviously, you have to do that in order to use USB for serial communications. As a quick reminder, this involves opening the microcontroller’s configuration GUI (your project’s .ioc file) and making the following changes :
· Enable the USB device peripheral you want to use.
· Under “Middleware”, enable the Communication Device Class middleware.
· Make sure the correct pins are correctly setup as USB D+ and D-.
· Make sure the USB device peripheral is correctly clocked.
As for the modifications to the middleware’s template source files, I’ve turned them into a library that you’re now going to integrate into your project. I’m not talking about coding, I’m talking about proper software engineering.
The bottom line is that you won’t have to copy and paste code from my website into your project. Instead, you’ll just clone my Git repository. Faster, safer, better.
Ideally, your STM32 project is itself a Git repository. Git is good. Everyone should use it. You now have two options : the amateurish or the pro.
An amateur would just copy my library’s source files into their own project and call it a day. It’s amateurish because in doing so, you lose all history for my library and won’t be able to pull future updates I might push to GitHub. You also won’t be able to easily switch between different versions. And if you make changes of your own that you think I’d like, you won’t be able to make a pull request for me to criticize. If that’s how you want to do it, good luck, I don’t have time to help you.
A pro would clone my library into their project as a Git submodule. That means your project and my library remain two separate repositories, each with their own development history and connection to their respective developers. Your own commits will simply record which version of my library they go with. Thus, you keep the option to easily pull new versions into your project. It’s more comfortable and it can’t break your code irreversibly.
I’m going to assume you’d rather work like a pro and tell you how to do that.
2. Clone the Repository
You will find my repository at :
https://github.com/Nefastor-Online/STM32_VCP
There are different ways to clone a repo; and you may already have your preferred Git tool. The simplest method is to use the command line. Enter the “git clone” command along with the repository URL you get from GitHub :
Open a terminal, navigate to the root of your STM32 project and enter the command :
git clone https://github.com/Nefastor-Online/STM32_VCP.git
Lo and behold, my source code is now in your project as a folder named STM32_VCP. Two details :
First, you can rename that folder. The exact name of the folder doesn’t matter to Git or to the code. I like to call it VCP, short and sweet. You might have different taste.
Second, you must make sure you’re checking out the correct version of the library. At the time of writing, this library has only one branch and one version. However, I might create more in the future :
· My library is built on top of STM32Cube libraries provided by ST. There is a risk that future versions of those libraries will break mine, requiring me to update my code.
· I may also need to create different branches to support different STM32 families as the needs arise.
Therefore, after cloning, you may want to use the “git branch” and “git tag” commands to list all existing versions and checkout the one you need. The repo’s README file also contains a list of hardware that has been tested and with what version.
3. Setup the Project
Cloning created a subfolder in your STM32CubeIDE project. However, at this point, it’s just a normal folder like your “Release” or “Debug” output folders. Its contents won’t be compiled when you build your project.
You can see that from the difference in icon :
Your freshly-cloned library needs to have a little “C” on its folder icon. Let’s give it one.
Note : if you’re not seeing the folder, hit F5 to refresh the view.
In the Project Explorer pane, right-click on the library’s folder and click on “Properties”. Navigate the dialog box to “C/C++ Build”. Unless you have a reason not to, make sure to select “[All configurations]”. Finally, untick “Exclude resource from build” :
This will ensure that the IDE compiles every source file it finds in that folder. Click “Apply and Close”.
We also have a header file in there. So let’s also add the folder to the project’s include paths : right-click the folder again and select “Add/remove include path…” : the rest is self-explanatory.
4. Declare the Submodule
I’ve mentioned Git submodules earlier. They are simply nested repositories, or “repos within repos”. This feature of Git was designed with libraries in mind. It solves a fundamental problem : since a library is meant to be used in multiple projects, it has to have a development history separate from all those projects.
I know a lot of people who don’t find the concept intuitive so I’ve come up with a relatable allegory.
Suppose you get hired by a company as an employee. That company has a history and you have your own, in the form of your resume. But your past doesn’t become that of your new employer upon joining. Looking at the company’s history; the only relevant historical information about you is the date your employment started and the skills that got you the job.
Now replace “company” with “project”, “employee” with “library”, “skills” with “features” and you’re much closer to intuiting the concept of a Git submodule.
In practice, Git will ignore any change to your project if it’s in a submodule. It will only tell you if the submodule contains uncommitted changes or if you’ve changed the version of the submodule you’re using. Should you modify a submodule, you will need to commit it first (to record the modifications), then commit your project (to record that it’s using the modified submodule).
To get this behavior from Git, you just need to tell your project’s repository that the nested repository is a submodule. Navigate to your project folder (same path where you cloned the repo) and tell your repo to add a submodule. If you haven’t renamed it yet, the command line will be :
git submodule add ./STM32_VCP
The command takes the path to the submodule, either absolute or (as in this example) relative to where you’re executing the command, hence the “./”.
The “git status” command will return, among other things, something like this :
The addition of a submodule is a change to your project, and that change must be committed.
If you’re still unclear about submodules, then I suggest reading the official documentation at https://git-scm.com/book/en/v2/Git-Tools-Submodules
If you came here looking for information on how to integrate one of my libraries that isn’t the USB VCP library for STM32, congratulations : you’re done, here. Everything that follows is specific to the aforementioned library. Happy trails ! Otherwise, you still have more work to do : read on !
5. Delete Template Files
If you’ve been paying attention, you’ve certainly noticed that cloning my library has resulted in a case of split personality. Indeed, you now have two different usbd_cdc_if.c and usbd_cdc_if.h files in your project :
· Those that STM32CubeIDE created when you generated the code for your project. Remember, they live under “USB_DEVICE/App” in your project.
· Those that you’ve just cloned.
Needless to say, you can only keep one set. Needless to say, you need to keep the set you just cloned.
You must therefore delete the two files located under “USB_DEVICE/App”.
Keep in mind that you’ll need to delete those two files every time you regenerate the code for your project. Don’t worry if you forget : your project will fail to compile, and then you’ll remember why. I haven’t found an elegant way to tell ST’s code generator not to generate them. If you know how to do that, I’d appreciate it if you told me. My contact form isn’t just meant for asking questions, it’s also for giving tips (of both the technical and monetary sort) and compliments.
6. Integration With Your Code
At this point, the library is ready to compile as part of your project. All that’s left to do is use it, and that’s the easy part. You can find the whole VCP API in usbd_cdc_if.h, it’s just four functions :
/* USER CODE BEGIN EXPORTED_FUNCTIONS */ void vcp_init (); int vcp_send (uint8_t* buf, uint16_t len); int vcp_recv (uint8_t* buf, uint16_t len); void vcp_service (); /* USER CODE END EXPORTED_FUNCTIONS */
Initialization, transmission, reception, and a service routine. If it were any more concise it would be a haiku.
First, make sure that you’re including the usbd_cdc_if.h header in the source files where you want to use USB serial communications. Typically, that would be “main.c”.
Because you’ve enabled the USB device and middleware, STM32CubeIDE generated some of the relevant code, but not all of it. Here’s an excerpt of a typical “main.c” file generated with USB support :
/* Includes ------------------------------------------------------------------*/ #include "main.h" #include "usb_device.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "usbd_cdc_if.h" /* USER CODE END Includes */
The IDE generated the first two include directives. The last one you may have to type in yourself. As usual, your own code must always go between USER CODE comments.
Next, you must call the library’s initialization function, vcp_init. Obviously, it must be called before you can transfer data.
Finally, you must call the library’s service function, vcp_service, at regular intervals. The easiest way to do this is from the System Tick (SysTick) interrupt service routine SysTick_Handler, which runs automatically every millisecond. You’ll find it in your project’s “Core/Src/stm32yyxx_it.c” file, along with all your ISR. It should end-up looking like this :
void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ // USB VCP library : background processing of data transmission vcp_service (); /* USER CODE END SysTick_IRQn 1 */ }
You will also need to #include “usbd_cdc_if.h” for the compiler to find the function.
Now that’s done, congratulations : at long last, you’re ready to write code that can talk to your PC through USB. We’ll look at how it’s done using a very simple example : a serial echo application.
7. Demonstration : Serial Echo
We’ve gone through a lot of setup and code, now it’s time to check if it all works or if I just wasted your time. Spoiler alert : I haven’t.
The simplest test of an RS232 port (or UART, or COM port) is the echo. Take every byte sent to the STM32 and send it back where it came from. This single test verifies that both reception and transmission work.
In your main function’s infinite loop you just need to add :
uint8_t buf[1000]; vcp_init (); while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // VCP demonstration - Echo all data received over VCP back to the host int len = vcp_recv (buf, 1000); // Read up to 1000 bytes if (len > 0) // If some data was read, send it back : len = vcp_send (buf, len); } /* USER CODE END 3 */
Build and Flash your project to your STM32, then connect its USB port to your PC.
Warning : before plugging the USB cable and depending on how your board is designed, you may need to make adjustments to the hardware. This is particularly true for the USB OTG port on all Nucleo boards !
Much earlier, we have checked that our PC detected the STM32 as a COM port, but it doesn’t hurt to check again. Besides, we’re going to need its COM port number (or tty device name, if you’re using Linux) in order to connect to it.
Next, we need software on our PC to talk to a serial device. We’ll use a simple terminal emulation program. I recommend TeraTerm : it’s simple, no-frills, and unlike PuTTY it actually respects the baud rate you set. Also, unlike PuTTY, it doesn’t lose its marbles when its serial connection is lost. That’s very important if you’re going to play with VCP : any time you recompile and Flash, PuTTY will force you to click a dialog box, close it and restart it. Tera Term will just sit there and wait for your STM32 to return to active duty.
You can download Tera Term for free from many different places, I’ll let you Google it. At the time of writing, I’m using version 4.91.
Once you’ve installed it, and made sure your STM32 is connected to a USB port, launch it. You should be greeted with this dialog :
Make sure you pick the right COM port and click OK. You should now be staring at a black window with a blinking cursor. Unless you’re stuck in the Matrix, don’t expect to suddenly get this :
If you do, don’t worry. Just give me five minutes to get into my S&M leather costume and find my sunglasses and I’ll be right with you with a bunch of colorful pills so we can discuss USB, plugging, unplugging and the nature of reality.
But seriously… back to work :
By default, terminal programs do not echo your keystrokes : that feature, known as “local echo”, interferes with any real-world application of serial communications. What you type goes out the COM port, and only what comes into the COM port will be displayed. Our STM32 running the echo code will, in effect, take your keystrokes and send them back so the terminal program can display them.
Don’t be shy, type a few things on the keyboard. If they end-up on the display, congratulations, you’ve got a working virtual COM port STM32 project. And the knowledge to add this feature to any or your future projects.
If you don’t get an echo, well, this has been a very long and tedious process. Maybe you missed a step. Maybe you fell asleep at the keyboard just like Neo. Drink some water, remind your significant other(s) that you’re still alive, and then take it from the top.
8. Fun With Testing
So. You’ve got some code that lets your STM32 echo keystrokes. But unless you’ve been bitten by a radioactive secretary we both know you’re not going to stress-test your STM32’s VCP just by typing. You may want to test sending a lot of data in one go and then verify that it all went through and back without errors.
The simplest and quickest way to do that is to send some ASCII art down the pipe. Because it’s an image (of sorts) you will be able to easily spot missing bytes from image distortion even as you send thousands of bytes in a row.
You can use this website : https://www.ascii-art-generator.org/ to turn any picture into ASCII art that you can then copy and paste into Tera Term. This will send the whole thing to your STM32 and display what the microcontroller received and sent back. I’m sexy so I usually use my own portrait for this :
If this works, it means both your circular FIFO’s work as they should. If it doesn’t, maybe your baud rate is too high, or maybe your Tera Term has a problem handling carriage returns (depends on your operating system) or maybe one or both of your FIFO’s doesn’t work quite right and you need to look at your code again. Too many possibilities for me to go through, but at this point you have all the knowledge you need to debug your way out.
9. A Quick Summary
Just to recap everything so far and get a high-orbit view of what’s going on :
First, you’ve configured our STM32 in STM32CubeIDE to enable its USB Full Speed device peripheral, and enabled the Communication Device Class (CDC) middleware. You’ve generated the code for your project, thereby adding all sorts of source files (some of them templates) to your project.
Because the CDC middleware is very generic, you’ve added a UART-like API by cloning this website’s VCP library into your project. This library added support of circular FIFO’s and functions to synchronize data transfer to (vcp_service) and from (CDC_Receive_FS) a USB host, using those FIFO’s. Those functions are essentially interrupt handlers and work in the background of your application, to keep CPU usage as low as possible.
The library also gave you functions to send (vcp_send) and receive (vcp_recv) data to and from the host at the application level.
Having built and flashed your project to your STM32 board, you have verified that you can actually connect to your STM32 through a virtual COM port.
This was quite a lot of work, so I think I’ll stop here for now… but there is still a lot to explore on the topic of ST’s CDC middleware. Expect another episode in the future, perhaps to cover different scenarios like FreeRTOS deployment and porting to specific STM32 families and boards.