Skip to content

Instantly share code, notes, and snippets.

@zaynkorai
Last active November 5, 2019 13:01
Show Gist options
  • Save zaynkorai/7559c745f0bbd0d41b3e3121e0a696e6 to your computer and use it in GitHub Desktop.
Save zaynkorai/7559c745f0bbd0d41b3e3121e0a696e6 to your computer and use it in GitHub Desktop.
7.1
I mentioned that the microcontroller has several pins. For convenience, these pins are grouped in ports of 16 pins. Each port is named with a letter: Port A, Port B, etc. and the pins within each port are named with numbers from 0 to 15.
The manual says:
LD3, the North LED, is connected to the pin PE9. PE9 is the short form of: Pin 9 on Port E.
LD7, the East LED, is connected to the pin PE11.
Up to this point, we know that we want to change the state of the pins PE9 and PE11 to turn the North/East LEDs on/off.
Each peripheral has a register block associated to it. A register block is a collection of registers allocated in contiguous memory. The address at which the register block starts is known as its base address. We need to figure out what's the base address of the GPIOE peripheral.
The table says that base address of the GPIOE register block is 0x4800_1000.
We are interested in the register that's at an offset of 0x18 from the base address of the GPIOE peripheral. According to the table, that would be the register BSRR.
The other thing that the documentation says is that the bits 0 to 15 can be used to set the corresponding pin. That is bit 0 sets the pin 0. Here, set means outputting a high value on the pin.
The documentation also says that bits 16 to 31 can be used to reset the corresponding pin. In this case, the bit 16 resets the pin number 0. As you may guess, reset means outputting a low value on the pin.
Correlating that information with our program, all seems to be in agreement:
-Writing 1 << 9 (BS9 = 1) to BSRR sets PE9 high. That turns the North LED on.
-Writing 1 << 11 (BS11 = 1) to BSRR sets PE11 high. That turns the East LED on.
-Writing 1 << 25 (BR9 = 1) to BSRR sets PE9 low. That turns the North LED off.
-Finally, writing 1 << 27 (BR11 = 1) to BSRR sets PE11 low. That turns the East LED off.
7.2
Reads/writes to registers are quite special. I may even dare to say that they are embodiment of side effects. In the previous example we wrote four different values to the same register. If you didn't know that address was a register, you may have simplified the logic to just write the final value 1 << (11 + 16) into the register.
Actually, LLVM, the compiler's backend / optimizer, does not know we are dealing with a register and will merge the writes thus changing the behavior of our program.
# same as cargo objdump -- -d -no-show-raw-insn -print-imm-hex -source target/thumbv7em-none-eabihf/debug/registers
$ cargo objdump --bin registers -- -d -no-show-raw-insn -print-imm-hex -source
How do we prevent LLVM from misoptimizing our program? We use volatile operations instead of plain reads/writes:
If you run it (use stepi), you'll also see that behavior of the program is preserved.
// A magic address!
const GPIOE_BSRR: u32 = 0x48001018;
// Turn on the "North" LED (red)
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);
// Turn on the "East" LED (green)
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);
// Turn off the "North" LED
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));
// Turn off the "East" LED
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
7.3
0x4800_1800
This address is close to the GPIOE_BSRR address we used before but this address is invalid. Invalid in the sense that there's no register at this address.
In most cases, exceptions are raised when the processor attempts to perform an invalid operation. Exceptions break the normal flow of a program and force the processor to execute an exception handler, which is just a function/subroutine.
There are different kind of exceptions. Each kind of exception is raised by different conditions and each one is handled by a different exception handler.
The aux7 crate depends on the cortex-m-rt crate which defines a default hard fault handler, named UserHardFault, that handles the "invalid memory address" exception. openocd.gdb placed a breakpoint on HardFault; that's why the debugger halted your program while it was executing the exception handler. We can get more information about the exception from the debugger.
most important one is pc, the Program Counter register. The address in this register points to the instruction that generated the exception.
r0 contains the value 0x4800_1800 which is the invalid address we called the read_volatile function with.
r0 is a CPU (processor) register not a memory mapped register; it doesn't have an associated address like, say, GPIO_BSRR.
7.4
BSRR is not the only register that can control the pins of Port E. The ODR register also lets you change the value of the pins. Furthermore, ODR also lets you retrieve the current output status of Port E.
If you run this program, you'll see:
$ # itmdump's console
(..)
-ODR = 0x0000
-ODR = 0x0200
-ODR = 0x0a00
-ODR = 0x0800
7.5
The fact the registers have different read/write permissions. Some of them are write only, others can be read and wrote to and there must be others that are read only.
Directly working with hexadecimal addresses is error prone. You already saw that trying to access an invalid memory address causes an exception which disrupts the execution of our program.
API should encode these three points I've mentioned: No messing around with the actual addresses, should respect read/write permissions and should prevent modification of the reserved parts of a register.
aux7::init() actually returns a value that provides a type safe API to manipulate the registers of the GPIOE peripheral.
As you may remember: a group of registers associated to a peripheral is called register block, and it's located in a contiguous region of memory.
In this type safe API each register block is modeled as a struct where each of its fields represents a register. Each register field is a different newtype over e.g. u32 that exposes a combination of the following methods: read, write or modify according to its read/write permissions. Finally, these methods don't take primitive values like u32, instead they take yet another newtype that can be constructed using the builder pattern and that prevent the modification of the reserved parts of the register.
// Turn on the North LED
gpioe.bsrr.write(|w| w.bs9().set_bit());
// Turn on the East LED
gpioe.bsrr.write(|w| w.bs11().set_bit());
// Turn off the North LED
gpioe.bsrr.write(|w| w.br9().set_bit());
// Turn off the East LED
gpioe.bsrr.write(|w| w.br11().set_bit());
Then we have this write method that takes a closure. If the identity closure (|w| w) is used, this method will set the register to its default (reset) value, the value it had right after the microcontroller was powered on / reset. That value is 0x0 for the BSRR register. Since we want to write a non-zero value to the register, we use builder methods like bs9 and br9 to set some of the bits of the default value.
you'll see that it produces exactly the same instructions that the "unsafe" version that used write_volatile and hexadecimal addresses did!
8 LEDs, again
just writing to BSRR was enough to control the LED
peripherals are not initialized right after the microcontroller boots.
you'll have to initialize configure GPIOE pins as digital outputs pins so that you'll be able to drive LEDs again.
8.1
Turns out that, to save power, most peripherals start in a powered off state -
The Reset and Clock Control (RCC) peripheral can be used to power on or off every other peripheral.
The registers that control the power status of other peripherals are:
-AHBENR
-APB1ENR
-APB2ENR
Your task in this section is to power on the GPIOE peripheral. You'll have to:
-Figure out which of the three registers I mentioned before has the bit that controls the power status.
-Figure out what value that bit must be set to,0 or 1, to power on the GPIOE peripheral.
-Finally, you'll have to change the starter code to modify the right register to turn on the GPIOE peripheral.
8.2
After turning on the GPIOE peripheral. The peripheral still needs to be configured.
if we want the pins to be configured as digital outputs so they can drive the LEDs; by default, most pins are configured as digital inputs.
You can find the list of registers in the GPIOE register block in:
The register we'll have to deal with is: MODER.
To further update the starter code to configure the right GPIOE pins as digital outputs. You'll have to:
-Figure out which pins you need to configure as digital outputs. (hint: check Section 6.4 LEDs of the User Manual (page 18)).
-Read the documentation to understand what the bits in the MODER register do.
-Modify the MODER register to configure the pins as digital outputs.
9 Clocks and timers
9.1
#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
for _ in 0..1_000 {}
}
The above implementation is wrong because it always generates the same delay for any value of ms.
-Fix the delay function to generate delays proportional to its input ms.
-Tweak the delay function to make the LED roulette spin at a rate of approximately 5 cycles in 4 seconds (800 milliseconds period).
-The processor inside the microcontroller is clocked at 8 MHz and executes most instructions in one "tick", a cycle of its clock. How many (for) loops do you think the delay function must do to generate a delay of 1 second?
-How many for loops does delay(1000) actually do?
-What happens if compile your program in release mode and run it?
9.2
There is a way to prevent LLVM from optimizing the for loop delay: add a volatile assembly instruction. Any instruction will do but NOP (No OPeration) is a particular good choice in this case because it has no side effect.
#[inline(never)]
fn delay(_tim6: &tim6::RegisterBlock, ms: u16) {
const K: u16 = 3; // this value needs to be tweaked
for _ in 0..(K * ms) {
aux9::nop()
}
}
And this time delay won't be compiled away by LLVM when you compile your program in release mode:
9.3
For loop delays are a poor way to implement delays.
Delays using a hardware timer. The basic function of a (hardware) timer is ... to keep precise track of time. A timer is yet another peripheral that's available to the microcontroller; thus it can be controlled using registers.
The microcontroller we are using has several (in fact, more than 10) timers of different kinds (basic, general purpose, and advanced timers) available to it.
Some timers have more resolution (number of bits) than others and some can be used for more than just keeping track of time.
Basic timers: TIM6
The registers we'll be using in this section are:
-SR, the status register.
-EGR, the event generation register.
-CNT, the counter register.
-PSC, the prescaler register.
-ARR, the autoreload register.
Here's a description of how a basic timer works when configured in one pulse mode:
-The counter is enabled by the user (CR1.CEN = 1).
-The CNT register resets its value to zero and, on each tick, its value gets incremented by one.
-Once the CNT register has reached the value of the ARR register, the counter will be disabled by hardware (CR1.CEN = 0) and an update event will be raised (SR.UIF = 1).
TIM6 is driven by the APB1 clock, whose frequency doesn't have to necessarily match the processor frequency.
That is, the APB1 clock could be running faster or slower. The default, however, is that both APB1 and the processor are clocked at 8 MHz.
The tick mentioned in the functional description of the one pulse mode is not the same as one tick of the APB1 clock.
The CNT register increases at a frequency of apb1 / (psc + 1) times per second, where apb1 is the frequency of the APB1 clock and psc is the value of the prescaler register, PSC.
9.4
With every other peripheral, we'll have to initialize this timer before we can use it.
To powering up the timer: We just have to set TIM6EN bit to 1. This bit is in the APB1ENR register of the RCC register block.
// Power on the TIM6 timer
rcc.apb1enr.modify(|_, w| w.tim6en().set_bit());
// OPM Select one pulse mode
// CEN Keep the counter disabled for now
tim6.cr1.write(|w| w.opm().set_bit().cen().clear_bit());
We'll like to have the CNT counter operate at a frequency of 1 KHz because our delay function takes a number of milliseconds as arguments and 1 KHz produces a 1 millisecond period. For that we'll have to configure the prescaler.
// Configure the prescaler to have the counter operate at 1 KHz.
Remember that the frequency of the counter is apb1 / (psc + 1) and that apb1 is 8 MHz.
tim6.psc.write(|w| w.psc().bits(psc));
9.5
First thing we have to do is set the autoreload register (ARR) to make the timer go off in ms milliseconds. Because the counter operates at 1 KHz, the autoreload value will be the same as ms.
// Set the timer to go off in `ms` ticks
// 1 tick = 1 ms
tim6.arr.write(|w| w.arr().bits(ms));
Next, we need to enable the counter. It will immediately start counting.
// CEN: Enable the counter
tim6.cr1.modify(|_, w| w.cen().set_bit());
Now we need to wait until the counter reaches the value of the autoreload register, ms, then we'll know that ms milliseconds have passed. That condition is known as an update event and its indicated by the UIF bit of the status register (SR).
// Wait until the alarm goes off (until the update event occurs)
while !tim6.sr.read().uif().bit_is_set() {}
This pattern of just waiting until some condition is met, in this case that UIF becomes 1, is known as busy waiting and you'll see it a few more times in this text :-).
Finally, we must clear (set to 0) this UIF bit. If we don't, next time we enter the delay function we'll think the update event has already happened and skip over the busy waiting part.
// Clear the update event flag
tim6.sr.modify(|_, w| w.uif().clear_bit());
10 Serial communication
This connector, the DE-9, went out of fashion on PCs quite some time ago; it got replaced by the Universal Serial Bus (USB).
So what's this serial communication? It's an asynchronous communication protocol where two devices exchange data serially, as in one bit at a time, using two data lines (plus a common ground). The protocol is asynchronous in the sense that neither of the shared lines carries a clock signal. Instead both parties must agree on how fast data will be sent along the wire before the communication occurs. This protocol allows duplex communication as data can be sent from A to B and from B to A simultaneously.
With the serial communication protocol you can send data from your laptop to the microcontroller.
This protocol works with frames. Each frame has one start bit, 5 to 9 bits of payload (data) and 1 to 2 stop bits. The speed of the protocol is known as baud rate and it's quoted in bits per second (bps). Common baud rates are: 9600, 19200, 38400, 57600 and 115200 bps.
To actually answer the question: With a common configuration of 1 start bit, 8 bits of data, 1 stop bit and a baud rate of 115200 bps one can, in theory, send 11,520 frames per second. Since each one frame carries a byte of data that results in a data rate of 11.52 KB/s. In practice, the data rate will probably be lower because of processing times on the slower side of the communication (the microcontroller).
Today's laptops/PCs don't support the serial communication protocol. So you can't directly connect your laptop to the microcontroller. But that's where the serial module comes in. This module will sit between the two and expose a serial interface to the microcontroller and an USB interface to your laptop. The microcontroller will see your laptop as another serial device and your laptop will see the microcontroller as a virtual serial device.
10.1
Connect the serial module to your laptop and let's find out what name the OS assigned to it.
dmesg | grep -i tty
But what's this ttyUSB0 thing? It's a file of course! Everything is a file in *nix:
ls -l /dev/ttyUSB0
You can send out data by simply writing to this file:
$ echo 'Hello, world!' > /dev/ttyUSB0
You should see the TX (red) LED on the serial module blink, just once and very fast!
Use the program minicom to interact with the serial device using the keyboard.
We must configure minicom before we use it. There are quite a few ways to do that but we'll use a .minirc.dfl file in the home directory. Create a file in ~/.minirc.dfl with the following contents:
$ cat ~/.minirc.dfl
pu baudrate 115200
pu bits 8
pu parity N
pu stopbits 1
pu rtscts No
pu xonxoff No
That file should be straightforward to read (except for the last two lines), but nonetheless let's go over it line by line:
pu baudrate 115200. Sets baud rate to 115200 bps.
pu bits 8. 8 bits per frame.
pu parity N. No parity check.
pu stopbits 1. 1 stop bit.
pu rtscts No. No hardware control flow.
pu xonxoff No. No software control flow.
minicom exposes commands via keyboard shortcuts. On Linux, the shortcuts start with Ctrl+A. On mac, the shortcuts start with the Meta key. Some useful commands below:
Ctrl+A + Z. Minicom Command Summary
Ctrl+A + C. Clear the screen
Ctrl+A + X. Exit and reset
Ctrl+A + Q. Quit with no reset
10.3
Connect the TXO and the RXI pins of the serial module together using a male to male jumper wire as shown above.
You should see three things:
As before, the TX (red) LED blinks on each key press.
But now the RX (green) LED blinks on each key press as well! This indicates that the serial module is receiving some data; the one it just sent.
Finally, on the minicom/PuTTY console, you should see that what you type echoes back to the console.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment