Использование OpenOCD для установки/снятия запрета чтения памяти CH32V20x

Запретить чтение памяти МК можно из кода программы, но для повторного программирования придется снять запрет. И все бы ничего, но под Linux, для микроконтроллеров WCH, нет решения «из коробки» для разблокировки памяти. Для преодоления этого неудобства появилось решение — расширить возможности скрипта OpenOCD для работы с МК.

Поиск в документации на МК по ключевому слову «protection» привел в раздел 32.6.4 Read Protection Release, посвященный снятию запрета на считывание памяти. В нем сказано, что необходимо сначала очистить область User Option Bytes, а затем записать в поле RDPR значение 0xA5. Чуть больше про это поле написано в описании регистров области User Option Bytes (рисунок 1).

Рисунок 1 — Описание регистра RDPR (раздел 32.6 User Option Bytes)

Рисунок 1 — Описание регистра RDPR (раздел 32.6 User Option Bytes)

Получается, нужно уметь очищать область User Option Bytes и записывать в него конкретное значение. Алгоритм очистки представлен в параграфе 32.6.3 User Option Bytes Erasure, а записи — в параграфе 32.6.2 User Option Bytes Programming. Осталось реализовать их средствами OpenOCD…

Коротко про OpenOCD

Для работы с регистрами OpenOCD предоставляет несколько функций (раздел 15.3 Memory access commands):

Чтение

Запись

Размер

mdw

mww

32 бита

mdh

mwh

16 бит

mdb

mwb

8 бит

Для чтения регистра целиком (32 бита), с сохранением в переменную, команда имеет вид:

set reg_data [ $_TARGETNAME.0 mdw $REG_ADDR ]

$_TARGETNAME.0 — конкретное целевое устройство, созданное командой target create.

При первом обращении к регистрам через OpenOCD выяснилось, что выдается не просто значение регистра, а еще и адрес регистра, и пустая строка (рисунок 2).

Рисунок 2 - Вид ответа OpenOCD при обращении к регистру

Рисунок 2 — Вид ответа OpenOCD при обращении к регистру

Возможно, это даже для чего‑то нужно, но не в моем случае. Поэтому для извлечения значения регистра написал такую функцию:

proc get_reg_value { data } {
    set reg_str [string trimright $data "\n "]
    lassign [split $reg_str ":"] reg out
    set hex 0x[string trim $out] 					# превращение в hex

    return $hex
}

Значение регистра возвращается в шестнадцатеричном формате, но в виде строки, поэтому о системе счисления лучше явно сообщить интерпретатору tcl. В противном случае результат будет интерпретирован как десятичное число.

Использование функций

Реализация алгоритма очистки области User Option Bytes получила название lock, а запись значения в поле RDPR — unlock:

Реализация

# 
# Return hex value of register
# 
proc get_reg_value { data } {
    set reg_str [string trimright $data "\n " ]
    lassign [split $reg_str ":"] reg out
    set hex 0x[string trim $out]

    return $hex
}

#
# Unlock any flash by register
#
proc _unlock_flash_by_reg { TARGET REG } {
    $TARGET mww $REG 0x45670123
    $TARGET mww $REG 0xCDEF89AB
}

#
# Unlock flash (FLASH_KEYR - FPEC key register)
#
proc _unlock_flash { TARGET } {
    set _FLASH_KEYR_ADDR    0x40022004

    _unlock_flash_by_reg $TARGET $_FLASH_KEYR_ADDR
}

#
# Unlock the User Option Bytes
#
proc _unlock_user_ob { TARGET } {
    set _FLASH_OBKEYR_ADDR  0x40022008

    _unlock_flash_by_reg $TARGET $_FLASH_OBKEYR_ADDR
}

set _TARGET $_TARGETNAME.0

set FLASH_CTLR_ADDR    0x40022010
set FLASH_STATR_ADDR   0x4002200C
set FLASH_OBR_ADDR	   0x4002201C
set RDPR_ADDR		   0x1FFFF800

# Status Register (FLASH_STATR)
set BSY_BIT_MASK	[expr {1 << 0}]
set EOP_BIT_MASK	[expr {1 << 5}]

# Control Register (FLASH_CTLR)
set LOCK_BIT_MASK	[expr {1 << 7}]
set OBWRE_BIT_MASK	[expr {1 << 9}]

# Selection word register
set RDPRT_BIT_MASK	[expr {1 << 1}]

set RDPR_MASK_LOCK	0x5aa5

# ms
set TIMEOUT_STEP	10
set TIMEOUT			500
#
# Read Protection Release
# 32.6.2 User Option Bytes Programming
#
proc unlock { } {
	set op "UNLOCK"
	
    global _TARGET
    
    global FLASH_CTLR_ADDR    
	global FLASH_STATR_ADDR
	global FLASH_OBR_ADDR   
	global RDPR_ADDR			

	# Status Register (FLASH_STATR)
	global BSY_BIT_MASK	
	global EOP_BIT_MASK

	# Control Register (FLASH_CTLR)
	set OBPG_BIT_MASK	[expr {1 << 4}]
	global LOCK_BIT_MASK	
	global OBWRE_BIT_MASK
	global RDPRT_BIT_MASK	

	global RDPR_MASK_LOCK

	# ms
	global TIMEOUT_STEP	
	global TIMEOUT			

	# Read protection status.
	# 0: Current read protection of flash is invalid;
    set RDPRT_byte [ get_reg_value [ $_TARGET mdb $FLASH_OBR_ADDR ] ]
    if { [ expr {$RDPRT_byte & $RDPRT_BIT_MASK} ] == 0 } {
        echo "** \[$op\] Memory is unlocked already **"
        return
    }
    
	# Erase the entire User Option Bytes area

	# 1)
	# Check the LOCK bit in the FLASH_CTLR register.
	# If it is 1, you need to perform the "Release Flash Memory Lock" operation.

	# Lock. When this bit is '1', it means that FPEC and FLASH_CTLR
	# are locked and cannot be written. After detecting the correct unlock sequence, the
	# hardware will clear this bit to ‘0’.
	# After an unsuccessful unlock operation, this bit will not change until the next system reset.

	set flash_ctrl_data [ get_reg_value [ $_TARGET mdb $FLASH_CTLR_ADDR ] ]
	if { [ expr {$flash_ctrl_data & $LOCK_BIT_MASK} ] == $LOCK_BIT_MASK } {
		echo "** \[$op\] FLASH_CTLR are locked. Need Release Flash Memory. Releasing ... **"		
		_unlock_flash $_TARGET
	} 
	
	# 2)
	# Check the BSY bit in the FLASH_STATR register to ensure that
	# there is no programming operation in progress.

	set timeout $TIMEOUT
	while { 1 } {
		set flash_statr_data [ get_reg_value [ $_TARGET mdb $FLASH_STATR_ADDR ] ]
		
		if { [ expr {$flash_statr_data & $BSY_BIT_MASK} ] != $BSY_BIT_MASK } {
		    break
		}

		if {$timeout == $TIMEOUT} {
			echo "** \[$op\] Flash memory operation is in the process. Waiting ... **"
		} elseif {$timeout == 0} {
			echo "\[$op\] !Err 1: Fail flash processing **"
			return 
		}

		incr timeout -$TIMEOUT_STEP
		after $TIMEOUT_STEP
	}

	# 3)
	# Check the OBWRE bit in the FLASH_CTLR register.
	# If it is 0, you need to perform the "User Option Bytes Unlock" operation.

	set flash_ctrl_data [ get_reg_value [ $_TARGET mdw $FLASH_CTLR_ADDR ] ]
	if { [ expr {$flash_ctrl_data & $OBWRE_BIT_MASK} ] == 0 } {
		echo "** \[$op\] User option bytes is locked **"
		_unlock_user_ob $_TARGET
	}

	# 4)
	# Set the OBPG bit in the FLASH_CTLR register to `1`.
	
	set flash_ctrl_data [ get_reg_value [ $_TARGET mdh $FLASH_CTLR_ADDR ] ]
    $_TARGET mwh $FLASH_CTLR_ADDR [expr {$flash_ctrl_data | $OBPG_BIT_MASK}]

	# 5)
	# Write the half word (2 bytes) to be programmed to the designated address.
	
    # Set correct RDPR code 0xA5 to the User Option Bytes
    $_TARGET mwh $RDPR_ADDR $RDPR_MASK_LOCK

	# 6)
	# When the BSY bit changes to '0' or the EOP bit in the FLASH_STATR register to be '1',
	# it indicates the end of programming. Clear the EOP bit to 0.
	set timeout $TIMEOUT
    while { 1 } {
    	set flash_statr_data [ get_reg_value [ $_TARGET mdb $FLASH_STATR_ADDR ] ]
    	
    	if {[ expr {$flash_statr_data & $BSY_BIT_MASK} ] != $BSY_BIT_MASK || [expr {$flash_statr_data & $EOP_BIT_MASK}] == $EOP_BIT_MASK} {
    		# Clear the EOP bit to 0.
			$_TARGET mww $FLASH_STATR_ADDR [expr {$flash_statr_data & !$EOP_BIT_MASK}]
			break
    	} 

		if {$timeout == $TIMEOUT} {
			echo "** \[$op\] Flash memory operation is in the process. Waiting ... **"
		} elseif {$timeout == 0} {
			echo "\[$op\] !Err 2: Fail flash processing **"
			return 
		}
		
		incr timeout -$TIMEOUT_STEP
		after $TIMEOUT_STEP
	}

	# 7)
	# Read the programming address data for verification.
	
    # State of RDPR byte in the User Option Bytes
    # RDPR_ADDR should be `5aa5` already
    set result [ get_reg_value [ $_TARGET mdh $RDPR_ADDR ] ]
    if { $result == $RDPR_MASK_LOCK} {
    	echo "** \[$op\] Memory unlocked **"
    } else {
    	echo "\[$op\] !Err 3: Fail memory unlocking **"
		return 
    }
}

proc lock { } {
	set op "LOCK"
	
	global _TARGET

    global FLASH_CTLR_ADDR    
	global FLASH_STATR_ADDR
	global FLASH_OBR_ADDR
	global RDPR_ADDR			

	# Status Register (FLASH_STATR)
	global BSY_BIT_MASK	
	global EOP_BIT_MASK

	# Control Register (FLASH_CTLR)
	set OBER_BIT_MASK	[expr {1 << 5}]
	set STRT_BIT_MASK	[expr {1 << 6}]
	global LOCK_BIT_MASK	
	global OBWRE_BIT_MASK
	global RDPRT_BIT_MASK

	global RDPR_MASK_LOCK
	set RDPR_MASK_UNLOCK		0xe339

	# ms
	global TIMEOUT_STEP	
	global TIMEOUT		


	# Read protection status.
	# 1: Current read protection of flash is valid.
    set RDPRT_byte [ get_reg_value [ $_TARGET mdb $FLASH_OBR_ADDR ] ]
    if { [ expr {$RDPRT_byte & $RDPRT_BIT_MASK} ] == $RDPRT_BIT_MASK } {
        echo "** \[$op\] Memory is locked already **"
        return
    }

    # 1)
	# Check the LOCK bit in the FLASH_CTLR register.
	# If it is 1, you need to perform the "Release Flash Memory Lock" operation.

	set flash_ctrl_data [ get_reg_value [ $_TARGET mdb $FLASH_CTLR_ADDR ] ]
	if { [ expr {$flash_ctrl_data & $LOCK_BIT_MASK} ] == $LOCK_BIT_MASK } {
		echo "** \[$op\] FLASH_CTLR are locked. Need Release Flash Memory. Releasing ... **"
		_unlock_flash $_TARGET
	}	

    # 2)
	# Check the BSY bit in the FLASH_STATR register to ensure that
	# there is no programming operation in progress.

	set timeout $TIMEOUT
	while { 1 } {
		set flash_statr_data [ get_reg_value [ $_TARGET mdh $FLASH_STATR_ADDR ] ]
		
		if { [ expr {$flash_statr_data & $BSY_BIT_MASK} ] != $BSY_BIT_MASK } {
		    break
		}

		if {$timeout == $TIMEOUT} {
			echo "** \[$op\] Flash memory operation is in the process. Waiting ... **"
		} elseif {$timeout == 0} {
			echo "\[$op\] !Err 1: Fail flash processing **"
			return 
		}

		incr timeout -$TIMEOUT_STEP
		after $TIMEOUT_STEP
	}

	# 3)
	# Check the OBWRE bit in the FLASH_CTLR register.
	# If it is 0, you need to perform the "User Option Bytes Unlock" operation.

	set flash_ctrl_data [ get_reg_value [ $_TARGET mdh $FLASH_CTLR_ADDR ] ]
	if { [ expr {$flash_ctrl_data & $OBWRE_BIT_MASK} ] == 0 } {
		echo "** \[$op\] User option bytes is locked. Need to unlock. Unlocking ... **"
		_unlock_user_ob $_TARGET
	}

	# 4)
	# Set the OBER bit in the FLASH_CTLR register to ‘1’,
	# then set the STRT bit in the FLASH_CTLR register to ‘1’,
	# to enable the user option bytes erasure.
	
    # Write OBER bit to 1 in FLASH_CTLR
    $_TARGET mwb $FLASH_CTLR_ADDR $OBER_BIT_MASK

    # Write STRT bit to 1 in FLASH_CTLR
    $_TARGET mwb $FLASH_CTLR_ADDR $STRT_BIT_MASK

    # 5)
	# When the BSY bit changes to '0' or the EOP bit in the FLASH_STATR register to be '1',
	# it indicates the end of programming. Clear the EOP bit to 0.
	set timeout $TIMEOUT
    while { 1 } {
    	set flash_statr_data [ get_reg_value [ $_TARGET mdb $FLASH_STATR_ADDR ] ]
    	
    	if {[ expr {$flash_statr_data & $BSY_BIT_MASK} ] != $BSY_BIT_MASK || [expr {$flash_statr_data & $EOP_BIT_MASK}] == $EOP_BIT_MASK} {
    		# Clear the EOP bit to 0.
			$_TARGET mww $FLASH_STATR_ADDR [expr {$flash_statr_data & !$EOP_BIT_MASK}]
			break
    	} 

		if {$timeout == $TIMEOUT} {
			echo "** \[$op\] Flash memory operation is in the process. Waiting ... **"
		} elseif {$timeout == 0} {
			echo "\[$op\] !Err 2: Fail flash processing **"
			return 
		}
		
		incr timeout -$TIMEOUT_STEP
		after $TIMEOUT_STEP
	}

	# 6)
	# Read the erasure address data for verification.
	
    # State of RDPR byte in the User Option Bytes
    # 0x1ffff800: e339e339
    set result [ get_reg_value [ $_TARGET mdh $RDPR_ADDR ] ]
    if {$result == $RDPR_MASK_UNLOCK} {
    	echo "** \[$op\] Flash succefully locked **"
    } else {
    	echo "\[$op\] !Err 3: Fail flash locking **"
    }

    # Clear the OBER bit to 0.
	set flash_statr_data [ get_reg_value [ $_TARGET mdw $FLASH_CTLR_ADDR ] ]
	$_TARGET mww $FLASH_CTLR_ADDR [expr {$flash_statr_data & !$OBER_BIT_MASK}]
}

Расширив оригинальный файл скрипта wch‑riscv.cfg (находится в папке IDE MounRiver_Studio_Community_Linux_x64_V170/MRS_Community/toolchain/OpenOCD/bin/) функциями lock и unlock можно вызывать их через IDE в окне настройки Run Configurations или Debug Configurations (в меню Run или на панели инструментов) во вкладке Startup (рисунок 3).

Так как MounRiver запускает сервер OpenOCD и GDB, обращение к функциям происходит через ключевое слово monitor.

Рисунок 3 - Окно настройки запуска проекта

Рисунок 3 — Окно настройки запуска проекта

Если биты блокировки памяти выставляются из программы МК, то функцию lock можно не вызывать.

Запрограммировать с предварительным снятием и последующей установкой защиты можно запустить командами OpenOCD:

$WCH_OPENOCD/openocd -f $WCH_OPENOCD/wch-riscv.cfg -c "init" -c "unlock" \
-c "program your_firmware.hex verify" -c "lock" -c "reset" -c "exit"

В скрипт добавил сообщения о текущих этапах, поэтому на выходе будет такая картина (рисунок 4), если память была закрыта.

Рисунок 4 — Лог при использовании функций lock и unlock в терминале

Рисунок 4 — Лог при использовании функций lock и unlock в терминале

Для блокировки без прочих операций с памятью:

$WCH_OPENOCD/openocd -f $WCH_OPENOCD/wch-riscv.cfg -c "init" -c "lock" -c "reset" -c "exit"

Ну и для разблокировки:

$WCH_OPENOCD/openocd -f $WCH_OPENOCD/wch-riscv.cfg -c "init" -c "unlock" -c "reset" -c "exit"

Некоторые наблюдения

В процессе работы с документацией обратил внимание на следующее:
в разделе 32.6.4 Read Protection Release сказано, что после очистки области User Option Bytes байт RDPRпримет значение 0xFF. Но нет. Как минимум на микроконтроллере ch32v203 поле принимает значение 0×39 (а регистр — 0xe339e339). Возможно, для каких то контроллеров, на которые распространяется документация, 0xFF является верным значением, но тогда было бы уместно уточнение.

© Habrahabr.ru