pyocd / pyOCD

Open source Python library for programming and debugging Arm Cortex-M microcontrollers
https://pyocd.io
Apache License 2.0
1.13k stars 484 forks source link

write_memory not changing memory state (STM32G071KBUxN) #1611

Open jacoblapenna opened 1 year ago

jacoblapenna commented 1 year ago

Setup

Custom board with STM32G0 MCU. PyOCD version = 0.35.1 Probe is a STLink/V3 with upgraded firmware version V3J1OM3B5S1

Symptoms & Implementation

I am trying to set MCU option bytes by writing to memory via the Python API. The specific option bytes and their address is stated in the MCU's reference manual. The following script provides a toy example:

from pyocd.core.session import Session
from pyocd.probe.stlink.usb import STLinkUSBInterface
from pyocd.probe.stlink_probe import StlinkProbe

probe = StlinkProbe(STLinkUSBInterface.get_all_connected_devices()[0])
session = Session(probe, options={"target_override": "stm32g071kbuxn"})
target = session.target

OPTION_ADDRESS = 0x1FFF7800
OPTION_SETTING = 0xFEFFFEAA

session.open()

before = target.read_memory(OPTION_ADDRESS)
target.write_memory(OPTION_ADDRESS, OPTION_SETTING, 32)
after = target.read_memory(OPTION_ADDRESS)

session.close()

assert after == OPTION_SETTING, print(f"Option bytes were not changed! Before write: {before:b}, After write: {after:b}, Desired: {OPTION_SETTING:b}")

I cannot get the assertion to pass. No exceptions are raised (aside from the purposeful assert). STM32CubeProgrammer can change the option bits with checkbox selection and I can read the change in the above script. Am I doing something wrong?

flit commented 1 year ago

STM32 option bytes are in flash memory, so you have to use the MemoryLoader flash programming class (or lower level equivalents) instead of regular memory transfers.

Here's an example based on your code above. Note there are two variants that use the different MemoryLoader APIs.

Variant 1—simple class method:

from pyocd.flash.loader import MemoryLoader
from pyocd.utility.conversion import u32le_list_to_byte_list

MemoryLoader.program_binary_data(session, OPTION_ADDRESS , option_data)

Variant 2—useful when adding data piece by piece with multiple .add_data() calls:

from pyocd.flash.loader import MemoryLoader
from pyocd.utility.conversion import u32le_list_to_byte_list

loader = MemoryLoader(session)
option_data = u32le_list_to_byte_list([OPTION_SETTING])
loader.add_data(OPTION_ADDRESS, option_data)
loader.commit()

Btw, you should use a with statement to open the session. This ensures everything (device, probe connection, etc) is properly closed if there's an exception.

hagibr commented 1 year ago

Did you really read STM32G0's Reference Manual (RM0454)? There's a correct procedure to unlock flash access, then unlock option bytes access, write new value in a register from FLASH group (don't write directly to address 0x1FFF7800) and then trigger the flash update and refresh. Here's my code snippet that I use in a project and I've adapted to your necessity:

# Table 5. STM32G0x0 peripheral register boundary addresses
FLASH_BASE = 0x40022000
# Table 17. FLASH register map and reset values
FLASH_KEYR = FLASH_BASE + 0x08
FLASH_OPTKEYR = FLASH_BASE + 0x0C
FLASH_SR = FLASH_BASE + 0x10
FLASH_CR = FLASH_BASE + 0x14
FLASH_OPTR = FLASH_BASE + 0x20

# 3.3.6 FLASH program and erase operation - Unlocking the Flash memory 
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB
# 3.4.2 FLASH option byte programming
OPTKEY1 = 0x08192A3B
OPTKEY2 = 0x4C5D6E7F

OPTION_ADDRESS = 0x1FFF7800
OPTION_SETTING = 0xFEFFFEAA

# Test if FLASH_OPTLOCK == 1
if( target.read32(FLASH_CR) & (1<<30) ):
    # First clear FLASH_LOCK
    target.write32(FLASH_KEYR, KEY1)
    target.write32(FLASH_KEYR, KEY2)
    # Then clear FLASH_OPTLOCK
    target.write32(FLASH_OPTKEYR, OPTKEY1)
    target.write32(FLASH_OPTKEYR, OPTKEY2)
    # Test if FLASH_OPTLOCK is still 1 (it shouldn't)
    if( target.read32(FLASH_CR) & (1<<30) ): 
        print("Failed to unlock")
        raise Exception("Failed to unlock")

# Writing the desired settings    
target.write32(FLASH_OPTR, OPTION_SETTING )

# Clearing possible previous error flags
target.write32(FLASH_SR, 0xFFFFFFFF)

# Triggering option byte write - Setting OPTSTRT = 1
target.write32(FLASH_CR, (1<<17))
time.sleep(0.01)
# Waits for BSY flag to be cleared
while( target.read32(FLASH_SR) & 0x01 ):
    pass
time.sleep(1)
# Restart target connection
target.init()
# Forces the loading of new configuration - Setting OBL_LAUNCH = 1 (what also resets IC)
target.write32(FLASH_CR, (1<<27))
time.sleep(1)
# Restart target connection
target.init()
flit commented 1 year ago

Did you really read STM32G0's Reference Manual

Nope, that's your job. 😉 I'm just going on the data available in the CMSIS-Pack. Beyond that, there's no way I have time to read the RM in detail for every MCU that someone asks about since I'm doing this on my free time.

Using the flash algorithm is provided in the CMSIS-Pack is the canonical and most reliable way to program the option memory. Of course, you can alway program flash memories by writing directly to the flash controller registers. The flash algorithms conveniently package these device specific register write sequences in a way that ensures they are performed correctly.

In any case, it seems you've got a working solution using direct register writes. That's great!

hagibr commented 1 year ago

Hello @flit, I think I've responded over your response instead of doing it directly to @jacoblapenna, what was my intention. I didn't think the algorithms that comes with CMSIS-Pack knew how to modify the option bytes (address 0x1FFF7800), because they are outside the normal flash region (addresses 0x08000000+). Well, creating my routine was good for learning, so it's a win-win.

jacoblapenna commented 1 year ago

Thanks for both your help on this. @hagibr yes, you are correct, I was not performing the unlock procedure 🤦. I have the following script that does change the option bytes (as verified in STM32CubeProgrammer after running the script), though with exceptions raised. The script follows the procedure as outlined in Section 3.4.2 of the reference manual. However, I still think there is an issue. Specifically, the last write, when I attempt to load the option bytes, I get pyocd.core.exceptions.TransferError: STLink error (22): DP error (stacktrace below). Is this because PyOCD is checking something from the write but the MCU is under reset from setting OBL_LAUNCH? I can handle this exception gracefully, but is there a better way (perhaps some setting I do not know about)?

Also, I do not fully understand what is happening here, why (after unlocking the flash) can I write directly to FLASH_BOUNDARY + FLASH_OPTR but you say I cannot write directly to 0x1FFF7800 (where the reference manual says the option bytes actually reside)? Indeed, 0x40022000 + 0x020 does not equal 0x1FFF7800, so I do not fully understand what is happening here. If you can share some knowledge, that would be much appreciated. Thank you!

from pyocd.core.session import Session
from pyocd.probe.stlink.usb import STLinkUSBInterface
from pyocd.probe.stlink_probe import StlinkProbe

probe = StlinkProbe(STLinkUSBInterface.get_all_connected_devices()[0])

# desired options bits
OPTION_SETTING = 0xFEFFFEAA

# 2.2.2 Memory map and register boundary addresses
# Table 6. STM32G0x1 peripheral register boundary addresses
FLASH_BOUNDARY = 0x40022000
# 3.7.22 FLASH register map
# Table 24. FLASH register map and reset values
FLASH_KEYR = FLASH_BOUNDARY + 0x008
FLASH_OPTKEYR = FLASH_BOUNDARY + 0x00C
FLASH_SR = FLASH_BOUNDARY + 0x010
FLASH_CR = FLASH_BOUNDARY + 0x014
FLASH_OPTR = FLASH_BOUNDARY + 0x020

# 3.3.6 FLASH program and erase operation
# Unlocking the Flash memory
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB
# 3.4.2 FLASH option byte programming
OPTKEY1 = 0x08192A3B
OPTKEY2 = 0x4C5D6E7F

# bit masks
BIT_16 = 1 << 16
BIT_17 = 1 << 17
BIT_27 = 1 << 27
BIT_30 = 1 << 30

with Session(probe, options={"target_override": "stm32g071kbuxn"}) as session:
    target = session.target
    before = target.read_memory(FLASH_OPTR)

    # unlock flash memory if locked
    if target.read32(FLASH_CR) & BIT_30:
        print("FLASH_OPTLOCK is set! Clearing option lock bit...")

        target.write32(FLASH_KEYR, KEY1)
        target.write32(FLASH_KEYR, KEY2)
        target.write32(FLASH_OPTKEYR, OPTKEY1)
        target.write32(FLASH_OPTKEYR, OPTKEY2)

        if target.read32(FLASH_CR) & BIT_30: 
            print("Failed to unlock")
            raise Exception("Failed to unlock")
        else:
            print("Option lock cleared, attempting to modify option bytes...")

    # write desired options
    target.write32(FLASH_OPTR, OPTION_SETTING)

    # wait for flash operation if busy
    while flash_sr := target.read32(FLASH_SR) & BIT_16:
        pass

    # set OPTSTRT bit
    target.write32(FLASH_CR, BIT_17)

    # wait for flash operation if busy
    while target.read32(FLASH_SR) & BIT_16:
        pass

    # set OBL_LAUNCH bit to load options under reset
    # DUBUG: this call raises: pyocd.core.exceptions.TransferError: STLink error (22): DP error
    target.write32(FLASH_CR, BIT_27)

    after = target.read_memory(FLASH_OPTR)

print(f"Before write: {before:b}, After write: {after:b}, Desired: {OPTION_SETTING:b}")
FLASH_OPTLOCK is set! Clearing option lock bit...
Option lock cleared, attempting to modify option bytes...
Error during board uninit:
Traceback (most recent call last):
  File "option_bytes_test.py", line 72, in <module>
    target.write32(FLASH_CR, BIT_27)
  File ".env/lib/python3.11/site-packages/pyocd/core/memory_interface.py", line 68, in write32
    self.write_memory(addr, value, 32)
  File ".env/lib/python3.11/site-packages/pyocd/core/soc_target.py", line 211, in write_memory
    return self.selected_core_or_raise.write_memory(addr, data, transfer_size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".env/lib/python3.11/site-packages/pyocd/coresight/cortex_m.py", line 514, in write_memory
    self.ap.write_memory(addr, data, transfer_size)
  File ".env/lib/python3.11/site-packages/pyocd/utility/concurrency.py", line 29, in _locking
    return func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".env/lib/python3.11/site-packages/pyocd/coresight/ap.py", line 1222, in _accelerated_write_memory
    self._accelerated_memory_interface.write_memory(addr, data, transfer_size,
  File ".env/lib/python3.11/site-packages/pyocd/probe/stlink_probe.py", line 290, in write_memory
    self._link.write_mem32(addr, conversion.u32le_list_to_byte_list([data]), self._apsel, csw)
  File ".env/lib/python3.11/site-packages/pyocd/probe/stlink/stlink.py", line 477, in write_mem32
    self._write_mem(addr, data, Commands.JTAG_WRITEMEM_32BIT, self.MAXIMUM_TRANSFER_SIZE, apsel, csw)
  File ".env/lib/python3.11/site-packages/pyocd/probe/stlink/stlink.py", line 467, in _write_mem
    raise self._ERROR_CLASSES[status](error_message)
pyocd.core.exceptions.TransferError: STLink error (22): DP error

UPDATE:

Even trying to handle the exception gracefully, I cannot perform any more operation in the session until I exit and reopen it once the error during board uninit is encountered. For example:

""" above script """

    try:
        target.write32(FLASH_CR, BIT_27)
    except TransferError:
        pass

with Session(probe, options={"target_override": "stm32g071kbuxn"}) as session:
    target = session.target
    after = target.read_memory(FLASH_OPTR)

print(f"Before write: {before:b}, After write: {after:b}, Desired: {OPTION_SETTING:b}")
hagibr commented 1 year ago

After POR, the OBL (Option Byte Loading) phase is executed, when the words at addresses 0x1FFF7800, 0x1FFF7818 and 0x1FFF7820 are evaluated to enable/disable some accesses, then loaded to FLASH_OPTR, FLASH_WRP1AR and FLASH_WRP1BR registers, respectively. To update them you must do this indirect procedure by first modifying the registers values then triggering a write back to the correct addresses. That's how it works. Maybe it's related to the need to make the words complementary (the word at 0x1FFF7800 must complement the word at 0x1FFF7804, the same with the words at 0x1FFF7818 and 0x1FFF781C, and the words at 0x1FFF7820 and 0x1FFF7824).

Try inserting target.init() before and after target.write32(FLASH_CR, BIT_27). I don't know why it's needed before, but it's sure to be needed after because we are triggering a reset by setting OBL_LAUNCH bit.

jacoblapenna commented 1 year ago

@hagibr thanks for that explanation. Also, I've tried every combination of target.init(), including:

In all cases, the outcome is the same, the exception is raised but the option bytes still get modified. Because the init does not appear to do anything in my case, I left it out for simplicity with no apparent change to the outcome. I've also tried target.reset() in various ways.

jacoblapenna commented 1 year ago

Update

Note that the exception issue corrupts the session itself and the corrupted session object cannot be reused. For example:

""" imports and address declarations here """

session_obj = Session(probe, options={"target_override": "stm32g071kbuxn"})

with session_obj as session:
    """ modify option bytes here """

with session_obj as session:
    """ session_obj is corrupt from writing to OBL_LAUNCH so this fails """
    target = session.target
    after = target.read_memory(FLASH_OPTR)

The last with statement raises the exception because the entire session is corrupt somehow. The only way to succeed is to declare an entirely new session object:

""" imports and address declarations here """

session_obj = Session(probe, options={"target_override": "stm32g071kbuxn"})

with session_obj as session:
    """ modify option bytes here """

new_session_obj = Session(probe, options={"target_override": "stm32g071kbuxn"})

with new_session_obj as session:
    """ this works with the new session object """
    target = session.target
    after = target.read_memory(FLASH_OPTR)
jacoblapenna commented 1 year ago

Additionally, the script raises pyocd.core.exceptions.TransferFaultError: Memory transfer fault (write) @ 0x40022008-0x4002200f with the first write to FLASH_KEYR if the the MCU already has firmware in it (i.e. after I download the main firmware to the MCU and try to modify option bytes again, this exception is raised). The script works again (i.e. modifies option bytes) if I perform a chip erase.

EDIT:

This is likely because flash is busy when firmware is loaded and I am not halting correctly or something. My script does not initially check if flash is busy.