Skip to content
This repository has been archived by the owner on Sep 27, 2021. It is now read-only.

Commit

Permalink
minor changes to tutorials
Browse files Browse the repository at this point in the history
  • Loading branch information
cdalizadeh committed Oct 19, 2019
1 parent 1345a25 commit ef4d451
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 31 deletions.
7 changes: 7 additions & 0 deletions Tutorials/README.md
@@ -0,0 +1,7 @@
# Tutorials

The tutorials in this folder will walk you through the process of setting up your development environment and learning the basics of embedded development.

1. [Getting Started with Cube and System Workbench](https://github.com/utra-robosoccer/soccer-embedded/blob/master/Tutorials/tutorial_1_getting_started.md)
2. [Communication Between MCU and PC](https://github.com/utra-robosoccer/soccer-embedded/blob/master/Tutorials/tutorial_2_communication.md)
3. [Getting Started with FreeRTOS](https://github.com/utra-robosoccer/soccer-embedded/blob/master/Tutorials/tutorial_3_freertos.md)
30 changes: 16 additions & 14 deletions Tutorials/tutorial_1_getting_started.md
@@ -1,30 +1,32 @@
# Getting Started with Cube and System Workbench

This page walks through the process of creating microcontroller projects using the Cube and System Workbench flow.

# Overview of the Tools
## Cube
## Overview of the Tools
### Cube
Cube is graphical tool that helps us initialize our microcontroller peripherals (e.g. ADCs, timers, communication modules). As per our configuration, Cube generates peripheral initialization code that uses ST's hardware abstraction layer (HAL) API. It is a nice convenience, but you'll find that you'll still need access to datasheets for the microcontroller and the development board you're using. For example, although you can configure timer prescalers and preload values in Cube, you'll need to use the microcontroller's datasheet to find which clock domain drives the timer. Similarly, you'll need to look at your development board's datasheet to figure out how the microcontroller pin names map to the development board pin names.

To download Cube:
1. Go to https://www.st.com/en/development-tools/stm32cubemx.html
2. At the bottom of the page click "Get Software". Login or register as needed
3. The download should automatically begin; if not, repeat the process once logged in

## System Workbench
### System Workbench
System Workbench is where the programming happens. It is an Eclipse-based IDE, and it comes with all the tools required to program and debug STM32 microcontrollers.

To download System Workbench:
1. Go to http://www.openstm32.org and create an account
2. Go to http://www.openstm32.org/Downloading%2Bthe%2BSystem%2BWorkbench%2Bfor%2BSTM32%2Binstaller
3. Choose the appropriate installer for your system (note: the Windows version is compatible with Windows 10)

## STLink USB Driver
### STLink USB Driver
You will need this USB driver to program code into the microcontroller. To download it, follow the link below and click "Get Software" at the bottom.
https://www.st.com/en/development-tools/stsw-link009.html

# Creating Your First Project
## Creating Your First Project
This tutorial will demonstrate how to use Cube and System Workbench to create a LED blinking program for a STM32L432KC microcontroller on a Nucleo-L432KC development board.

## Cube: Selecting the Development Board
### Cube: Selecting the Development Board
First, we want to create a new Cube project for the Nucleo-L432KC development board:
1. Open Cube and select New Project
2. In the New Project window, choose the Board Selector tab and type in "nucleo-l432kc" in the search bar
Expand All @@ -36,7 +38,7 @@ This will open up the view in the image below.

![Cube](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/2-Blank-Project.jpg)

## Cube: Project Settings
### Cube: Project Settings
Next, we want to set up code generation & toolchain settings:
1. In the top panel, select Project -> Settings ...

Expand All @@ -48,37 +50,37 @@ Next, we want to set up code generation & toolchain settings:

3. In the code generator tab, check the option "Generate peripheral initialization as a pair of '.c/.h' files per peripheral"

![Project Settings 2](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/4-Project-Settings-2.jpg)
![Project Settings 2](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/5-Project-Settings-2.jpg)

4. Click OK to exit to save current settings and exit the project settings window

## Cube: Peripheral Initialization
### Cube: Peripheral Initialization
At this point, our project is all set up and we can see Cube automatically tells us which pin is connected to the LED. Thus, this example program does not really require peripheral configuration. We can still dive a bit deeper by going to the Configuration tab and opening up the Pin Configuration window. From this window, we can change the GPIO operation frequency, default value (low or high), mode, and label. The label allows you to set an alias for the pin which you can use to make your code more readable.

![Pin Alias](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/6-LED-Pin.jpg)

![GPIO Config](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/7-GPIO-Config.jpg)

## Cube: Code Generation
### Cube: Code Generation
We can now generate the Eclipse project and peripheral initialization code. To do this, click Project -> Generate Code. Choose the option "Open Project" to open the project in the System Workbench IDE. You may need to create a new Eclipse workspace. It is suggested that you use 1 workspace for all embedded projects. You can import and remove projects from a workspace as needed to keep it organized.

### Known manual adjustments after generating code
#### Known manual adjustments after generating code

CubeMX does not always behave as expected, and modifications made to the project may be overwritten when re-generating code. Below is a list of known settings that must be changed back manually after re-generating code on a project:

- Compiler optimization settings for a project are by default set to the `-O3` (optimize most) flag by Cube. We are using `-Og` (optimize for debug) for our Debug builds (we currently program the Debug build on the robot) (see [related](https://github.com/utra-robosoccer/soccer-embedded/issues/128#issuecomment-439647194)). This must be manually changed back by going to project `Properties -> C/C++ build -> Settings -> Tool settings -> <MCU ... Compiler> -> Optimization`. Do this for both the MCU GCC and MCU G++ compilers.

![Generate Code](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/8-Generate-Code.jpg)

## System Workbench: a Look at the Files
### System Workbench: a Look at the Files
Upon opening the project in the System Workbench, we can take a look at some key files in the Src and Inc directories:
- `Src/main.c`: contains the `main` function, from which all program execution begins. We notice this function automatically calls some peripheral initialization functions before entering a while loop which user code can go into
- `Src/gpio.c`: implements functions for initializing the GPIO peripherals
- `Inc/stm32l4xx_hal_conf.h`: public interface for the entire microcontroller. This header includes a lot of others, so essentially any file which includes this has access to all the HAL APIs

![System Workbench Project Explorer](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/9-System-Workbench-Project-Explorer.jpg)

## System Workbench: Toggling the LED
### System Workbench: Toggling the LED
Let's open up stm32l4xx_hal_gpio.h. This can be found inside the Drivers folder. Inside the Outline panel in the System Workbench, we can see all the functions declared in this file (Window -> Show View -> Outline). These declarations form the public interface for the GPIO peripherals.

![GPIO APIs](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/10-GPIO-APIs.jpg)
Expand All @@ -102,7 +104,7 @@ while (1)
}
```

## System Workbench: Compiling & Programming
### System Workbench: Compiling & Programming
To compile, either press the hammer icon or right-click the project in the Project Explorer and choose the Build Project option.

![Building](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_1/12-Compiling.jpg)
Expand Down
20 changes: 11 additions & 9 deletions Tutorials/tutorial_2_communication.md
@@ -1,15 +1,17 @@
# Communication Between MCU and PC

This page walks through how to move bytes back and forth between a PC and MCU. For instructions setting up the development environment and running a simple microcontroller program, please see the [previous tutorial](https://github.com/utra-robosoccer/soccer-embedded/wiki/STM32:-Getting-Started-with-Cube-and-System-Workbench).

# Overview
## Overview
All Nucleo development boards come with integrated programmers (for the L432KC, this is located on the bottom). This is a diverse chip, with the ability to flash new code into the MCU, debug code running on it, and also bridge UART communication on the MCU side with USB communication on the PC side. Essentially, the microcontroller can send bytes via UART, and the programmer chip will convert it to a USB packet and send it to the PC. The USB protocol defines a software interface called a virtual COM port (VCP) which allows an application to interact with a device as though it were streaming raw bytes. Serial monitor programs such as Docklight can then display these raw bytes to a user, perhaps formatted as human-readable strings. The same idea holds for sending messages to the microcontroller from the PC.

Later tutorials on FreeRTOS will build on top of this one.

# PC Side
## PC Side
For this walkthrough, we will either need a serial terminal program or a custom script that sends and receives bytes via virtual serial port (e.g. PySerial). A popular cross-platform serial terminal is PuTTY. One that works nicely for Windows and provides a rich GUI is Docklight (download: https://docklight.de/downloads/). We will use Docklight in this tutorial.

# MCU Side
## Peripheral Configuration (Cube)
## MCU Side
### Peripheral Configuration (Cube)
Continuing from the last tutorial, our project should be all set up. In fact, the default peripheral initialization already enables the USART which is connected to the programmer, USART2.

![VCP Pins](https://raw.githubusercontent.com/utra-robosoccer/soccer-embedded/master/Tutorials/Images/tutorial_2/1-USART2-Pins.jpg?raw=true)
Expand All @@ -26,7 +28,7 @@ Also in the USART2 window, we will enable interrupts from the NVIC Settings tab

We can then generate the code as before.

## Polled Transmission (TX)
### Polled Transmission (TX)
The `HAL_UART_Transmit()` function is the blocking API for sending bytes via UART. We'll show how to use it first then make a few important points after.
```C
const uint32_t timeout = 10; // ms
Expand All @@ -49,7 +51,7 @@ Important points:

![Clock Configuration](https://github.com/utra-robosoccer/soccer-embedded/blob/master/Tutorials/Images/tutorial_2/5-Clock-Config.jpg?raw=true)

## Polled Reception (RX)
### Polled Reception (RX)
The `HAL_UART_Receive()` function is the blocking API for receiving bytes via UART. In this example, we'll change the LED state from the PC by sending either 0 or 1 as ASCII.
```C
const uint32_t rx_timeout = UINT32_MAX; // ms
Expand Down Expand Up @@ -79,7 +81,7 @@ Important points:
- The program does not do _anything_ until a byte is received, hence the maximum RX timeout. That is the sad reality of blocking I/O
- We call the variable `ascii_value` a buffer

## Interrupt-Based TX and RX
### Interrupt-Based TX and RX
Suppose we would like to be able to send "Hello world!" to the PC twice per second while also being able to change the LED state by sending a command from the PC. If we were only able to use polled I/O, we would not be able to accomplish this reliably as we would be constrained to transmit and receive at separate times. To get around this, we use the interrupt-based APIs `HAL_UART_Transmit_IT()` and `HAL_UART_Receive_IT()`. Both of these functions will _initiate_ a transfer of bytes, then return and allow the CPU to continue executing instructions from the caller's context. Basically, the CPU operates in parallel with the UART hardware while it sends a single byte. When the byte is done being transmitted, the UART hardware generates an interrupt, and the auto-generated interrupt handler will load the next byte into the UART hardware, and the cycle begins again.

Since `HAL_UART_Receive_IT()` returns immediately after being called, we need a way of checking whether it it is done before using the buffer. This can be done by polling the UART handle's state in our application loop, or by means of a _callback_. We'll look at the former option first, and will use the latter option when we go through DMA.
Expand Down Expand Up @@ -124,7 +126,7 @@ Important points:
- Interrupt-based APIs allow us to initiate data transfers in parallel with each other and with the CPU. For a UART, such transfers are constrained to the length of a byte, so after a byte is sent or received, the CPU is interrupted for a brief period of time to handle the event. This is a huge improvement compared to the blocking I/O cases illustrated above, as now the system can go do other things. Usually, interrupts occur infrequently relative to the core operating frequency, and interrupt handling code is small
- The `HAL_GetTick()` returns the number of ms the program has been operating for (this is tracked by the "SysTick" hardware timer, which is always running). We can use this to time-trigger events
## DMA-Based TX and RX
### DMA-Based TX and RX
The DMA controller is a module completely distinct from the CPU. The CPU talks with the DMA controller to initiate data transfers of several bytes long, in general. Once the CPU performs this initial step, the DMA controller performs the entire transfer itself, and notifies the CPU via interrupt once it is entirely done. Imagine a scenario where you have 5 UARTs sending and receiving several bytes simultaneously at 1 Mbps. This would generate a ton of interrupts, causing the CPU to be frequently diverted from the application code. DMA has an obvious advantage in this case since the CPU is only needed at the start and the end of transfers. One caveat is that the CPU and DMA controller have to share the system bus to access memory, so if the CPU is involved in memory-intensive tasks the bus contention may cause a slowdown.
The UART DMA APIs are very similar to the others for the most part. Sending uses `HAL_UART_Transmit_DMA()` and receiving uses `HAL_UART_Receive_DMA()`. One difference of note is that these can be configured to use _circular_ buffering. For example, if circular receive is used, then calling `HAL_UART_Receive_DMA()` will cause bytes to be read from the RX line into the buffer forever. This is good in cases where an unknown amount of data needs to be received or where the application must continuously be subscribed to the RX line.
Expand Down Expand Up @@ -186,7 +188,7 @@ Important points:
- Since we are using circular DMA, we only need to initiate the reception once. In a real-world situation, you'd probably want to implement the error callback to restart the transfer if there were communication errors
- The reception event processing can all be moved into the callback, which more closely resembles how we envision the system's operation in an abstract sense

## Circular RX DMA For Variable-Sized Packets
### Circular RX DMA For Variable-Sized Packets
In the previous example, we saw how DMA allows us to stay continuously subscribed to a source of information while at the same time offloading the CPU's involvement in the communication process. The DMA channel `CNDTR` register tells us (indirectly) how many bytes have been received into the buffer. Specifically, it is a downcounter which starts at the transfer size passed into `HAL_UART_Receive_DMA()` as the third argument, and each time a new byte is received it is decremented. When `CNDTR` equals 1, the next reception will cause it to wrap around the number line back to its initial value. In this way, reception can continue forever and applications can treat the receive buffer like a FIFO.

```C
Expand Down

0 comments on commit ef4d451

Please sign in to comment.