I've had to look this up a couple times, I always forget the specifics. This post will serve as a quick reference for the basic process I use, and some pitfalls you can run into. There is already a lot of content online for this process, and I specifically like this guide by Cannonkeys.
I use Ubuntu primarily, so I prefer command line tools. Using ST's nice little graphical upload tool would probably be easier for some of this, but I recommend getting into the command line if you're going to be doing this for a long time.
Basic Process
I use an ST-Link V2 for this, they are quite cheap and easy to use. There is also a little utility you can get (stlink-tools) that makes this easy to use on the command line. In Ubuntu, you can just do:
sudo apt-get install stlink-tools
And then you have access to a few more commands that can make your life much easier. The st-flash utility is a neat way to program the STM32 on the blue pill board, the other main alternative is to try using opencd I believe. You still can use openocd to do things like debug and set lock bits on the device, but I haven't had much luck programming the stm32 on the board.
To actually flash an STM32F103C8 board like the blue-pill, you'd use this command:
st-flash --reset --format binary \
write generic_boot20_pc13.bin file 0x8000000
The 'generic_boot20_pc13.bin' is the stm32duino bootloader, which is a fork ("derivation") of the maple bootloader. The file is available from their github repository, along with a lot of other bootloaders for different configurations and different devices.
To actually program the devices with the Arduino IDE, you'll need the stm32 board files from this board url: http://dan.drown.org/stm32duino/package_STM32duino_index.json
Don't forget to actually install the board files.
For whatever reason, the STM32 specific blink example didn't work for me, but the default Arduino blink example worked great.
Clones
A real annoying thing you run into buying cheap STM32f103 boards is the chinese clones. I would recomend buying genuine ST devices, but some of the clones do work. I think at this point the GD32f103 is one of the most common. It's actually pretty good, and may work perfectly fine.
I had some devices that would program just fine, but would not do USB properly. I was trying to run them in keyboards, and so of course the USB connection was sort of necessary. They would have been fine for most Arduino projects, but yeah just beware of that.
Write-Protection
Note: This section is entirely optional and can be disregarded if you don't need write protection.
I worked on figuring this out for a while, and eventually realized these ARM processors probably don't work the same way as the AVR processors I use more often. In the AVR processors, the program memory and program storage is accessible in the same way (I believe). On Arduinos, AVR processors, I feel like in the past I've accidentally corrupted the bootloader and/or my own program. I have also accidentally bricked the bootloader on SAMD21 processors, but that was specifically while working with their flash. Still, I shouldn't have been able to brick the bootloader even if I was specifically working with flash. Write-protecting the bootloader sector just seems like good practice, and something I've been meaning to learn more about in general, anyway.
I'm not well versed in bootloaders in general, let alone how the STM32 bootloader works. It does seem to me that it would be nice to re-lock the bootloader region after flashing it. There does seem to be write-protection related memory registers mentioned in the reference manual, but I couldn't find the complete documentation for it there. Application note AN4838 is about write-protection, and seems to talk about solving this exact problem. Application note PM0075 also talks about this, and indeed actually has some useful details.
This table shows the flash pages, starting from 0x0800_0000 which is where we put the bootloader. The bootloader is about 22kB, which means it should be using about 22 pages (23, since it's a bit more than 22kB). That means, I think, that we want to lock pages 0-22. We have a granularity of 4 pages for medium-density devices like the STM32f103c8, so the best we can do is 0-23, I believe. This would be WRP0 = 0b0011_1111, I think. Actually it'd be 0b1100_0000, since write protection happens when set to 0. If we go ahead and read the register (0x1FFFF808) with openocd, we can see what it's normally set to:
And, it appears to be all set to 1s. That would make sense since there shouldn't be any default write protection that I'm aware of. Well, it would sort of make sense. These option bytes should be paired with their inverse, so it should go something like 0x00FF_00FF. I looked around a bit more after finding that, and found my way to the PM0068 application note (so much reading!). It contained this great table:
As it happened, essentially the same table was already contained in PM0075. As you can see, the reset value for the register is indeed all ones. If we read the FLASH_OBR register, we should see something ending in two zeros. I think I was also reading this incorrectly, the base address for these registers is actually 0x4002_2000 and I was assuming it was the option bytes address 0x1FFF_800. Using the new address, we get data (like the OBR register) that makes more sense I think.
That does indeed end in 2 zeros, and is mostly all set to 1 like we'd expect. So, setting the WRPR register via this method would include setting the address '0x40022020' to whatever you wanted to write protect, I think. The application notes reference having to unlock the flash configuration registers somehow. This person has got it figured out for a separate device it looks like. You have to sort of unlock the registers, and then I think you configure them through a control register. His specific process appears to be, write to FLASH_OPT
KEYR twice, then check FLASH_SR, then write to FLASH_OPTCR1 and FLASH_OPTCR, finally check status again (FLASH_SR) to see if the operations are done.
For our specific device (STM32F103C8), the two keys written to FLASH_KEYR are 0x4567_0123 and 0xCDEF_89AB and that unlocks the "Flash Program and Erase Controller" (FPEC). To write to to the write-protection bytes, App. note PM0075 says we have to write the lock keys to the OPTKEYR, which turns out to be the same keys we just used in KEYR? That should set OPTWIRE in FLASH_CR to 1, then we need to set OPTPG in FLASH_CR.Then I think we can directly write to the '0x1FFF_800' base address section to setup write protection. Hopefully we don't accidentally turn on read-protection, which can permanently brick the device I believe. This snippet (entered into telnet connected to openocd) is what I've arrived at:
# This will write-protect 32 pages (larger than most bootloaders)
# Send keys to FLASH_KEYR
mww 0x40022004 0x45670123;
mww 0x40022004 0xCDEF89AB;
# Send the keys to OPTKEYR
mww 0x40022008 0x45670123;
mww 0x40022008 0xCDEF89AB;
# Verify status
mdw 0x4002200C;
# Verify OPTWRE bit is set (0x00000200)
mdw 0x40022010;
# Set the OPTPG bit in the FLASH_CR register
mww 0x40022010 0x00000210;
# Verify status
mdw 0x4002200C;
# Verify OPTPG bit
mdw 0x40022010;
# Write data to flash (half-word)
mwh 0x1FFFF808 0xFF00;
# Wait for the BSY bit to be reset ?
# Verify programmed value (0xFFFFFF00)
mdw 0x1FFFF808;
# Reset and leave :)
reset; exit;
To test whether or not this works, we can try setting some bits there in the first 23 pages or so. Attempting to write to locked flash should raise the WRPRTERR error in FLASH_SR. First though, let's just try writing to flash and retrieving the value. I'm gonna try writing on the start of page 33, which should be safely outside of the locked 0-31 pages. (Went ahead and locked all of WPR0 for now). The base address for main memory is 0x0800_0000, and each page is 1k. Therefore 33 pages is 0x8008400.
Before we can write here though, we have to go ahead and enable writing to flash. I think this fundamentally the reason why most people don't seem to both locking the bootloader, since it's inherently safe from most things by merit of being in the flash memory. Still, I imagine that when working with flash memory programmatically it might be possible to accidentally hit either program space, or the bootloader. Anyway, if we just try to write (half-word always!) to flash we get this:
Though, nothing gets set in the status register. Actually at this point I ran into a funny issue. Openocd seems unaware of any flash after a 0x8000 offset. Which is weird, since it should have 64k of flash, and 0x8000 is only 32k... Oh. I have a STM32F103C6 plugged into my ST-link. D'oh. I had swapped them in the drawer somehow, with the C8's separated out. Anyway, after plugging a C8 in and trying our raw flash write again, we don't get an error but nothing appears to actually have been written.
To actually write to flash, it actually has to be erased first. This test alone should be enough to verify write-protection actually. The basic procedure to erase a flash page is:
# This assumes the FPEC is unlocked
# Set the PER bit in the FLASH_CR register
mww 0x40022010 0x00000002;
# Program the FLASH_AR register to select a page to erase
mww 0x40022014 0x08008400;
# Set the STRT bit in the FLASH_CR register
mww 0x40022010 0x00000040;
# Wait for the BSY bit to be reset
mdw 0x4002200C;
# Read the erased page and verify (notice +1 to page address)
mdw 0x08008401;
The last result should indicate the page is erased by reporting it to be all FF's (all 1s). At this point we can try the same procedure on write-protected flash, and see what happens. I tried erasing page 16 (which is inside our 32 locked pages), and did indeed get a WPR error!
Neat. I also double checked and made sure that page hadn't been written to all 1s. To write something to flash after it's been erased, this seems to work:
# Send keys to FLASH_KEYR
mww 0x40022004 0x45670123;
mww 0x40022004 0xCDEF89AB;
# Verify status
mdw 0x4002200C;
# Verify LOCK bit is not set (0x00000000)
mdw 0x40022010;
# Set the PG bit in the FLASH_CR register
mww 0x40022010 0x00000001;
# Verify status
mdw 0x4002200C;
# Verify PG bit
mdw 0x40022010;
# Write data to flash (half-word)
mwh 0x08008400 0xFFAA;
# Verify programmed value
mdw 0x08008400;
The only thing left to do, is to lock the bootloader and see if I can still program it. It was at this point I discovered that the (now working) bootloader on the C8, rather than the C6, actually does load values in the write-protect option bytes. Unfortunately, it loads all 1s, which means it's all unprotected. And, I have to erase all of the option bytes in order to set them, which might break the bootloader?
The long and short of trying to actually lock the bootloader, is it's possible but you have to do everything in the right order. Once the write-protection bits are set, they cannot be programmed again without clearing the option bytes. Clearing the option bytes also disables reading the flash by enabling the read protection. So the process is, clear option bytes & reset, program ONLY read-protection to disable it & reset, load the bootloader & reset, and finally enable write-protection and reset. This is the code I use to do this with openocd:
## Erases option bytes, then re-enables reading
mww 0x40022004 0x45670123;# Send keys to FLASH_KEYR
mww 0x40022004 0xCDEF89AB;
mww 0x40022008 0x45670123; # Send the keys to OPTKEYR
mww 0x40022008 0xCDEF89AB;
mww 0x40022010 0x00000020; # Set the OPTER bit in the FLASH_CR register
mww 0x40022010 0x00000060; # Set the STRT bit in the FLASH_CR register
mdw 0x4002200C; # Check status, should be 0x20 EOP
reset; # Reset the device to finish erasing option bytes
mww 0x40022004 0x45670123; # Send keys to FLASH_KEYR
mww 0x40022004 0xCDEF89AB;
mww 0x40022008 0x45670123; # Send the keys to OPTKEYR
mww 0x40022008 0xCDEF89AB;
mww 0x40022010 0x00000210; # Set the OPTPG bit in the FLASH_CR register
mwh 0x1FFFF800 0x00A5; # Set the RDP to 0x00A5
reset;
mdw 0x1FFFF800; # Check read-protection result
##
## Upload bootloader at this point
##
## This will write-protect 2 pages, 8KB, for the stm32duino bootloader
mww 0x40022004 0x45670123; # Send keys to FLASH_KEYR
mww 0x40022004 0xCDEF89AB;
mww 0x40022008 0x45670123; # Send the keys to OPTKEYR
mww 0x40022008 0xCDEF89AB;
mww 0x40022010 0x00000210; # Set the OPTPG bit in the FLASH_CR register
mwh 0x1FFFF808 0x00FC; # Set write-protection
# Reset and leave :)
reset; exit;
mdw 0x1FFFF808; # Check write-protection result
I did verify that you can still load sketches onto the board, as well. Nothing too complicated, but hopefully that's fine. Anyway, this concludes my dive into write-protected flash on STM32s.
What I learned
I learned how to directly manipulate the flash memory in an STM32 device, an approach that should work no matter what the interface is; it's not just limited to openocd. I learned I can store data in the option bytes, and how I can erase pages of flash.
It's a good idea to use openocd's 'flash info' tool to figure out what you're working with. I should have started with that, and made sure there was an appropriate amount of flash on the chip. I learned that it's actually pretty difficult to accidentally mess with your bootloader (and program storage I assume) since flash is protected by the FPEC. I did learn that I can protect my bootloaders, but that it might be easier to set that protection from the bootloader itself because of the complicated edit-flash-edit process.
I did at first believe the stm32duino bootloader was a whooping 23kB, but as it turns out I missed the "bootloader only" binaries directory on the github. Oops. 8kB isn't bad for a bootloader on a device with 64kB available flash! QMK keyboard firmware only uses about 32kB. There are smaller bootloaders available, now that I look. The stm32-hid-bootloader is a good example, coming in at under 4kB! I'm sure there are instances in which you'd want that extra 4kB, but for most things the dfu bootloader works fine.
Comments