Take a look at the differences between Microprocessor (in our personal computer or phone) and Microcontroller
### Von Neumann vs Harvard architecture
efficiency: Harvard architecture can avoid the "Von Neumann bottleneck"
> VERBOSE~
**Von Neumann bottleneck**: when the bandwidth between CPU and RAM is much lower than the speed at which a typical CPU can process data, because the shared bus for instructions and data can cause competition.
In embedded system, harvard architecture is widely used. Our board (STM32F103C8T6) use harvard architecture on physical level (refer to the block diagram in reference manual)
>*VERBOSE~*
**ICode bus**:This bus connects the Instruction bus of the Cortex®-M3 core to the Flash memory instruction interface.
**DCode bus**:This bus connects the DCode bus (literal load and debug access) of the Cortex ®-M3 core to the Flash memory Data interface.
However, in the software level, we treat the instruction memory and data memory as a whole block of memory (therefore, it is more accurate to say that the stm32 uses a mixed Harvard and von Neumann architecture.).
In stm32, instruction memory, data memory, registers of peripherals/IO are all mapped to memory.
Table from https://embeddedsecurity.io/vendor-stm32
L3 (Programming)
Type Qualifiers
const
implies that value not supposed to be written by program (read only) during run-time. Can be modified by others like hardware.
If you want to save the limited RAM spaces (data memory) for other variables, you can use this keyword to store this variable in ROM (program memory). It’s important for harvard architecture.
volatile
indicate the value can be changed by something other than program so it should be reexamined frequently.
This means two things:
The compiler will not try to optimize the variable with volatile. See the two examples on slides.
Each time the program reads the volatile variable, the processor will not look into cached data memory, meaning that the program can always get the newest updated data in memory (which is very important when external hardware change the variable). However, this case is not relevant with STM32 MCU since it didn’t have cache.
What about const volatile ?
The combination of the above two concepts. Usually used to declare pointers Example: const volatile char *a declares a pointer pointing to a value that cannot be changed by the program through *a, but the value of a can be changed (pointing to another value). *a = 0 is not allowed, a = &b is allowed.
Generally, we use const volatile to declare pointers that points to hardware registers or memory-mapped Input ports(read only).
Basic Program Structure
1 2 3 4 5 6 7 8 9 10 11
// import header file for the board (containing the declaration of SFR) // declare global variables intmain(void){ // initialization (system clock, peripherals) while(true) // super loop, the the program alive { // interact with peripherals }
}
How to interact with peripherals
We need to use C code to set the value of SFR.
SFR (special function registers)
These are registers that are embedded in peripherals, used for configuration and control of peripherals.
VERBOSE~ If we want to get the status of a peripheral, we read the value of SFR. If we want to send something to peripheral, we write value to SFR.
Let’s take timer as an example: SFR in block diagram of timer:
SFR declaration in code:
We change operate with these registers through Bit Operation.
(These are PIC32 codes but I think you have got it)
L4 (IO)
All the modes of GPIO:
The whole block diagram:
You may see an unfamiliar unit
Here. It’s used to convert the input analog voltage to digital voltage. You can memorize it through this graph: Transfer function of a Schmitt trigger.
>The horizontal and vertical axes are input voltage and output voltage, respectively. T and −T are the switching thresholds, and M and −M are the output voltage levels.
Input
After we configured the GPIO to be input ports, the output driver is disabled (disconnected).
Pull down
- When the IO pin is connected to LOW (0V) or unconnected, the input data register will be 0.
- When the IO pin is connected to HIGH (3.3V/5V), the input data register will be 1.
Pull up
- When the IO pin is connected to LOW (0V), the input data register will be 0.
- When the IO pin is connected to HIGH (3.3V/5V) or unconnected, the input data register will be 1.
Floating
- When the IO pin is connected to LOW (0V), the input data register will be 0.
- When the IO pin is connected to HIGH (VDD), the input data register will be 1.
- When the IO pin is unconnected, the input data register will be unpredictable.
General Purpose Output
The input driver part is still enabled so that we can read the output status.
Open Drain
Can “generate” voltage higher than VDD at IO pin.
- “0” in the Output register activates the N-MOS (LOW (0V) at IO pin)
- “1” in the Output register leaves the port in Hi-Z (HIGH (V+) at IO pin)
(the P-MOS is never activated)
Push Pull
Most common one.
- “0” in the Output register activates the N-MOS (LOW (0V) at IO pin)
- “1” in the Output register activates the P-MOS (HIGH (VDD) at IO pin)
Alternative Function Output
Not covered.
L5 (Interrupts)
Why interrupt?
Most of the peripherals take quite a few time to complete its task or trigger an event. Instead of instruct the processor to keep checking the status of these peripheral (polling), we want the peripherals to inform processor when there exists an event, so that the processor can focus on its own task.
Peripherals inform the processor through external interrupt.
Where do interrupts come from
We mainly deal with the interrupts from peripherals.
Each peripheral can have multiple interrupt sources, indicating different events.
How to handle interrupts
Through interrupt service routine (ISR)
#### Interrupt vectors
Interrupt vectors are **addresses that inform the interrupt handler as to where to find the ISR**
This is an arbitrary IVT that depicts the pattern of IVT, for detailed IVT of STM32, please refer to the reference manual.
What to do inside ISR
Always remember to clear the interrupt flag in ISR.
Because there is often more interrupt sources than interrupt vectors, you need to judge which source triggered this interrupt based on interrupt status register.
Customized operation… (but be short, because if the operation take too many clock cycles, it may be interrupted by another interrupt source, which may not be on purpose)
Nested interrupts
When executing ISR, the processor can be interrupted by interrupts with higher priority.
> **Remainder:** the lower the priority number, the higher the priority
L6 (Timer)
Let’s split Timer peripheral into 3 parts:
#### Blue part: Master/slave controller
The master/slave unit provides the time-base unit with the **counting clock signal** (for example the CK_PSC signal, PSC here means that it's for the prescaler in time-base unit), as well as the counting direction (counting up/down) control signal.
This unit mainly provides the **control signals** for the time-base unit.
#### Yellow part: Time-base unit
The main block of the programmable timer is a **16-bit** counter with its related auto-reload register.
The counter can count up, down or both up and down.
The counter clock can be divided by a **prescaler** (which is basically another counter).
> On the reference manual there are many wave-forms for you to understand how these control registers take effects.
The reset frequency of the counter is $$\frac{f_{input}}{(Prescaler+1)\times(Counter Period+1)}$$
Red part: Timer-channels unit
The timer channels are the working elements of the timer. They are the means by which a timer peripheral interacts with its external environment (through input capture or output compare).
How to use multiple timer for more counting digits (32-bit)
It is possible to configure one slave timer to increment its counter based on a master-timer events such as the timer update event. The master-timer event is signaled by the master timer master/slave controller unit. This controlling unit uses the master timer output-TRGO signal. The master timer output-TRGO signal is connected to the slave timer TRGI-input signal. The master/slave controller unit of the slave timer is configured to use the TRGI-input signal as clock source to increment the slave timer counter.
L7 (LCD)
Just refer to RC2_LCD. I think it’s not the focus of final exam.
L9 & L10 (Input Capture & Output Compare)
IC is a peripheral that can monitor the input signal changes (pos/neg edge) independent of the processor (Core). OC is a peripheral that can generate precise output signal independent of the processor (Core).
In STM32, it is embedded in timer peripheral (together with output compare).
IC
You can consider IC as a timer value recorder. It will record the timer value each time the capture condition is met (you can see from the diagram, the timer value is from CNT counter).
These conditions can be:
rising edge
falling edge
both With a prescaler, we trigger capture events every few edges. Similar for interrupt, we can trigger an interrupt every few captures.
Note: The IC does not capture the edge immediately when a rising or falling edge happened. The capture event needs to be sync with PB_clk. Further more, the module will capture the timer counter value that is valid 2-3 PB_clk cycles after the capture event.
For detailed configuration, please refer to Reference Manual.pdf on canvas, page 349-359, 382-385.
OC
Just refer to RC2_Output_Campare and RC3_Lab4 for the concepts and PWM configuration. Also, the solution of hw2 has been uploaded to canvas, please take a look.
We need to generate PWM signal on PB14(embeded with an LED D1, or D2 if you like)
> when the `PB14` output `LOW`, `D1` will light up.
In STM32, Input Capture and Output Compare is configured in Timer channels.
Let’s check which Timer is connected to PB14
It's the `CH2N` channel of timer 1.
Then we configure timer 1.
Prescaler: 7200-1
Counter Period: 200-1 (50Hz)
Pulse: 180 (duty cycle of 90%; TIM_CNT 0~179: HIGH, TIM_CNT 180~199: LOW)
CHN Polarity: LOW (needed for CHN)
> CHxN is the complementary channel of CHx in Timer1.
> If the polarity of CHxN and CHx are different, the two channels will follow the same output pattern, otherwise they will be complementary
Also remember to configure PB12 (SW4) as GPIO_Input.
> You can try to use Input Capture to capture the press action of the button (as indicated in lab manual, i.e. connecting `PB12` to `PA0`).
## Code
Init PWM in `main()`:
1 2 3 4
/* USER CODE BEGIN 2 */ HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_2); /* USER CODE END 2 */
Get the press/release action of the button (PB12):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
int pressed = 0; /* USER CODE BEGIN WHILE */ while (1) { // main loop /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if (!HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) && pressed == 0) { pressed = 1; // what to do when pressed } elseif (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) && pressed == 1) { pressed = 0; // what to do when releassed } } /* USER CODE END 3 */
Posted Updated Notea few seconds read (About 75 words)
VDD (Pin 2): Power supply (usually +5V). Note: If you use J-link debugger to power the board, the output voltage at +5V pin will not be 5V, maybe around 2.8V, so remember to use the 5V power supply wire to power the board.
VO (Pin 3): Contrast adjustment pin. Usually connected to a potentiometer to adjust the contrast.
RS (Pin 4): Register select pin. Low for instruction register, high for data register.
RW (Pin 5): Read/Write pin. Low for write operation, high for read operation. Usually grounded (write mode).
E (Pin 6): Enable pin. The LCD controller only captures (grabs) the data presented at its register lines(D0-D7) only when the E pin “transitions” from high to low.
D0-D7 (Pins 7-14): Data bus pins. Used for communication with the microcontroller in either 4-bit (D4-D7) or 8-bit (D0-D7) mode.
A (Pin 15): For the backlight. Typically connected to +5V.
K (Pin 16): For the backlight. Typically grounded (GND).
voidLCD_init(void) { LCD_Write_Command(LCD_2_LINE_8_BITS); // function set -8 bit interface Delay_ms(5); LCD_Write_Command(LCD_2_LINE_8_BITS); LCD_Write_Command(LCD_CLR_DSP); // clear display Delay_us(100); LCD_Write_Command(LCD_CSR_INC); // Set entry mode: increment Delay_us(100); LCD_Write_Command(LCD_DSP_CSR); // open display, close cursor }
Display Charactor
First we need to set the cursor position
The cursor position is basically the DDRAM address that you want to write data in, which is corresponding to the display location on LCD screen.
1 2 3 4 5 6 7 8 9
voidLCD_Set_Position(uchar x, uchar y) { if (y == 0) // first line { LCD_Write_Command(0x80 + x); } elseif (y == 1) // second line { LCD_Write_Command(0xc0 + x); } }
If we want to display a character at first line, first column:
1
LCD_Set_Position(0,0);
Then, we need to write data into DDRAM[addr] to tell the LCD which character you want to display, for example “J”.
Modify the required jdk version in file stm32cubemx.sh from exec archlinux-java-run --min 17 -- -jar /opt/stm32cubemx/STM32CubeMX "$@" to exec archlinux-java-run --min 17 --max 20 -- -jar /opt/stm32cubemx/STM32CubeMX "$@"
Then build and install the STM32CubeMX
1
makepkg --noconfirm --skipinteg -si
Since STM32CubeMX is not compatible with jdk22 (which is the default jdk that arch is currently using), you need to install jdk17 through yay -S jdk17-openjdk
Then you can start STM32CubeMX by running stm32cubemx, and hopefully, everything is fine.
Compiler
Use arm-none-eabi-gcc
1 2
yay -S arm-none-eabi-gcc yay -S arm-none-eabi-newlib
Debugger
Use OpenOCD to burn and debug STM32 through STLink v2 (the blue USB device provided by us).
1
yay -S openocd
Setup Your STM32 Project
Open your STM32CubeMX, follow the instruction of Lab1.pdf to configure your project.
NOTE: In Project Manage -> Project -> Project Settings -> Toolchain / IDE, use Makefile/CMake.
Generate the code and go to the project directory (with Makefile/CMakeLists.txt in the directory).
Then you need to generate the compile_commands.json for clangd to recognize the project.
Makefile
1
bear -- make
CMake
1
cmake -S ./ -B ./build
Build Project
Makefile
1
make
Then target binary file is ./build/<Project Name>.bin
CMake
1
cmake --build ./build
Then target binary file is ./build/<Project Name>.elf
Open On-Chip Debugger 0.12.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'. Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD Info : clock speed 1000 kHz Info : STLINK V2J37S7 (API v2) VID:PID 0483:3748 Info : Target voltage: 3.222587 Info : [stm32f1x.cpu] Cortex-M3 r1p1 processor detected Info : [stm32f1x.cpu] target has 6 breakpoints, 4 watchpoints Info : starting gdb server for stm32f1x.cpu on 3333 Info : Listening on port 3333 for gdb connections [stm32f1x.cpu] halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x08000dc8 msp: 0x20005000 ** Programming Started ** Info : device id = 0x20036410 Info : flash size = 64 KiB ** Programming Finished ** ** Resetting Target ** shutdown command invoked
NOTE: In different Distro, the cfg file for OpenOCD may locate in different directories. You need to find it by yourselves.
By the way, if you use CMake
Note: When uploading binary file to STM32, it’s recommended to use .bin file instead of .elf file. Please use the following script to convert the .elf to .bin and upload.
To use segger ozone, you need a different linker called jlink (originally we use st-link v2). You need to buy this linker first (maybe on Taobao or Amazon).
Program File: select the binary file you have built (.elf is recommended).
Debug:
set some breakpoints and watch some variables of your interest. Press the green “power” icon on the upper left corner to start (upload the program and start the debugging process) Press the blue “play” icon besides “power” to continue.
Posted Updated a few seconds read (About 31 words)