玩轉 pyocd

哈拎發表於2021-08-21

(一) pyocd

(1) 什麼是pyocd

​ pyocd 是 arm 開發的一個 python 包(python package),該軟體包可以使用多種USB偵錯程式對 arm cortex-M 微控制器進行除錯、程式設計(燒錄程式)。該軟體包還是跨平臺的,支援Linux、Mac、Windows。

也就是說,可以通過 pyocd 使用一些偵錯程式來除錯、擦除、燒錄基於 arm cortex-M(M0/M3/M4/M7/M23/M33) 核心的微控制器。目前支援 Daplink、ST-Link、jlink。

目前從 pyocd github 上看到的,pyocd 內部(預設)支援70多個常用的微控制器,但是通過使用 CMSIS-Packs 幾乎支援所有基於 comtex-m 核心的微控制器。

​ 這裡不關注 pyocd 除錯功能,只關注把 pyocd 用作上位機來操作 MCU 的功能,可以通過兩種方式使用 pyocd:

  • pyocd 提供了命令列工具,使用這工具可以用來除錯、燒錄、擦除 MCU
  • pyocd 提供了python API,可是使用這些API來實現控制 MCU(讀取MCU暫存器、暫停、執行、復位MCU等)

(2) 安裝

​ pyocd 支援的系統: windows、linux、Mac

由於 pyocd 是基於 python,所以需要先安裝 python,pyocd 支援的 python 版本為:Python 3.6.0 or later.

安裝方法有:

  • 下載 pyocd 的原始碼安裝

從 GitHub 獲取原始碼:

git clone https://github.com/mbedmicro/pyOCD.git

進入pyOCD目錄,使用如下命令安裝:

python setup.py install
  • 通過pip安裝
pip install -U pyocd

python3 -mpip install --pre -U git+https://github.com/pyocd/pyOCD.git@develop

參考:https://pypi.org/project/pyocd/

(3) Libusb

daplink V1 使用的是 HID 協議,只要安裝pyocd是可以用的,如果使用 daplink V2、ST-Link、j-link 的話,需要安裝 libusb,pyocd 裡面的文件給出了安裝方法。

我的做法是把 libusb.dll 複製到 python 安裝目錄:

(二)如何使用pyocd命令列工具

首先開啟命令列,windows 中可以是命令提示符或者 powershell,如下:

如果安裝了 git,也可以使用 git bash,如下:

pyocd 的使用方法是在命令列中輸入:

pyocd + 子命令 + 引數

在命令列中輸入 pyocd 或者 pyocd -h 回車,如下圖,就會輸出 pyocd 的使用方法及 pyocd 的子命令:

根據上面說明,看下我安裝的版本:

從上圖中可以看到 0.30.2 版本有 9 個子命令,但其中 commander 跟 cmd、gdbserver 跟 gdb 是相同功能的。所以,總的來說有 9 個功能不同的子命令。分別是:

  • commander跟cmd: 可互動的終端,
  • erase: 擦除命令
  • flash:燒錄命令
  • reset:復位裝置
  • gdbserver跟gdb:用於gdb除錯的
  • json:以json格式輸出資訊
  • list:可以列出dap-link、目標IC、特定板子的資訊
  • pack:用於管理CMSIS-Pack
  • server

如果想知道上述命令的用法可以使用 pyocd + 上述中的一個命令 + -h/--help,如下:

這些命令各有什麼用呢?什麼情況下要用什麼命令?這些命令怎麼配合使用?

比如,使用 j-link 給 MCU 下載韌體,一般會使用上位機 j-flash 通過 jlink 下載,首先 上位機 j-flash 能找到 j-link,然後需要選擇所下載的 MCU,然後選擇所要下載的韌體,這 3 步都設定好後,就可以下載程式了,

根據這些,依次介紹 pyocd 的命令:

  • list or json:檢視能夠使用的偵錯程式、支援的 MCU
  • pack:管理支援的 MCU
  • flash & erase : 對 MCU 進行程式設計、擦除

(1)pyocd list 命令

從 pyocd 幫助資訊來看:

list List information about probes, targets, or boards.

可以知道 list 命令使用來檢視 偵錯程式、目標晶片、板子的資訊,

  • pyocd list / pyocd list -p

列出連線到電腦上的 dap link 或者 ST link,當沒接如任何pyocd所支援的裝置時,如下:

接入了一個 daplink 跟 jlink 後如下:

(1.1) pyocd list -t/--target

列出所支援的IC,可以是內建的,也可以是通過安裝pack獲得支援的,

上面圖片展示了列出了 pyocd 內建的一部分 MCU。

我的電腦中已近安裝了 GD32F350 的pack,上圖中,GD32F350 系列後面就顯示了 pack

(1.2) pyocd list -b/--board

列出所支援的板子,如下:

(2)pyocd json 命令

json Output information as JSON.

以 json 格式輸出資訊,輸出什麼資訊呢? 看下 json 命令幫助:

從上圖可以看到 pyocd json 一共可以輸出4中資訊:probes、targets、boards、features。分別看下這4中會輸出什麼。

(2.1)pyocd list -p

當電腦沒有接入任何 pyocd 支援的裝置時:

可以看到輸出了所安裝的 pyocd 版本資訊,接了一塊ST的開發板後,如下:

可以看到,輸出的除了 pyocd 版本資訊外,還有所接板子的資訊:板子上偵錯程式的ID、板的名字等。

再試一下,接一塊沒有連線目標 MCU 的 Daplink,輸出的資訊如下:

(2.2) pyocd json -t

從幫助資訊來看,是輸出所有已知的 target,先試下pyocd json -t命令,如下:

輸出一大堆資料,除了有pyocd 版本資訊外,就是一些MCU的資訊,有MCU的廠商、型號,還有該資訊是內建的還是來自pack。

試下能不能輸出指定 MCU 的資訊:

從上述結果來看,好像是不行。

pyocd list 跟 pyocd json 功能應該是差不多的,都是輸出一些資訊,如接入了電腦的偵錯程式資訊,系統所支援的微控制器等,只不過輸出方式不同,pyocd list 是直接輸出相關資訊,pyocd json 是以 json 格式輸出相關資訊。

(3)pyocd pack 命令

pyocd 通過兩種方法支援 MCU,一個是內建的(builtin),這個數量有限,還有一個是通過 pack 來支援,需要用pyocd 操作什麼 MCU,安裝對應的 pack , 類似 keil5 ,新安裝的 Keil5 不支援任何微控制器,如果要使用新安裝的keil 支援某型別號微控制器,需要安裝對應的軟體包。

pyocd 提供了一個內建的子命令來對pack進行管理,安裝、查詢、刪除等,來看下 pyocd pack 命令的幫助資訊:

送上圖來看,有5個功能選項:

  • -c:清楚儲存在電腦上的pack資訊
  • -u:更新pack索引
  • -s:顯示已安裝的pack
  • -f:查詢某個IC對應的pack
  • -i:安裝指定的pack

pyocd 使用的 pack 有個預設的存放路勁,我電腦上為:

C:\Users\Administrator\AppData\Local\cmsis-pack-manager\cmsis-pack-manager

pyocd pack 命令就是對該目錄的檔案進行管理。

(3.1)pyocd pack -U

使用方法為:

pyocd pack -u
或者
pyocd pack --update

第一次使用該命令的時候,會在pyocd存放pack檔案的預設路勁下下載不同廠商不同系列 MCU 的 pack 的描述檔案(pdsc檔案)和 index.json、aliases.json 檔案。執行過一次之後,會從網路上更新相應的檔案。下圖是一個執行pyocd pack -u的結果,有出現錯誤。

下圖是執行了pyocd pack -u 後,pack 所在資料夾多了很多檔案:

(3.2)pyocd pack -f

使用方法為:

pyocd pack -f partnumber

該命令會顯示出對應型號 MCU 的 pack 的資訊,如下圖,顯示了 stm32f042 所對應 pack 的資訊

(3.3)pyocd pack -i

使用方法為:

pyocd pack -i partnumber

該命令安裝指定型號MCU的pack,如下圖:

不過,由於網路的問題,一般很難下載完成,跟keil下載pack一樣,非常慢,經常下載失敗,可以手動下載所需的pack,然後放到對應目錄。

(3.4)pyocd pack -s

使用方法為:

pyocd pack -s

列出所安裝的pack,如果還未安裝任何pack,該命令執行結果為:

安裝有pack後,執行結果為:

(4)pyocd flash 命令

測試硬體是 Daplink+STM32F042F4 最小系統,Daplink 我自己用 STM32F042F4做的,如下:

STM32F042F42 的測試程式是用 Cube MX 建立工程,由於板子上沒有其他外設,但是 Daplink 有 USB 轉 TTL,實現了個 STM32F042 Uart 輸出的測試程式,主要程式如下:

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		printf("STM32F042:%d\r\n",i);
		i++;
		HAL_Delay(1000);
  }
  /* USER CODE END 3 */

正常跑起來是這樣:

第一次測試,首先執行 pyocd list檢視是否成功識別到裝置,確認能夠找到 daplink 後。

pyocd flash -h 輸出的幫助資訊看不出來 pyocd flash 應該怎麼用:

非常多選項,這裡只關注燒錄的功能,通過查閱 pyocd 的文件,瞭解到可以使用如下命令來給MCU燒錄程式:

pyocd flash -t mcu_partnumber  firmware

我的 MCU 是 stm32f042f4,測試韌體是 pyocd_test.hex,嘗試使用命令 :

pyocd flash --t stm32f042f4 ./pyocd_test.hex來燒錄程式,結果如下:

結果顯示不支援 stm32f042f4,使用 list 檢視下目前環境下支不支援 042:

下載 stm32f042 的 pack:

可以找到 STM32F042 了,重新試下燒錄:

從這結果看是燒錄成功了,看了下板子,也成功跑起來了:

(5)pyocd erase 命令

pyocd 幫助資訊中對該指令的描述是:

erase Erase entire device flash or specified sectors.

也就是說可以使用該指令對 裝置 進行全擦或者指定扇區,pyocd earse幫助資訊如下:

我嘗試使用瞭如下命令:

(5.1) 整片擦除:-c/--chip

看了下板子已經沒有輸出了,

(5.2) 擦除扇區:-s/--sector

該命令需要加一個扇區地址,如果沒加的話,會不成功,如下是沒加地址的:

對於目標晶片,如果新增的地址不對,也會操作不成功:

新增正確的地址結果如下:

(5)使用 pyocd 操作 STM32F051

試下安裝 STM32051 的 pack,看下能不能通過安裝 STM32f051 的 pack 來使 pyocd 支援 STM32f051,首先檢視已安裝的 pack,查詢結果如下,

PS D:\project\USB\Daplink\my_dap\doc> pyocd pack -s
  Vendor   Pack   Version
---------------------------

從上述結果來看,尚未安裝任何 pack,然後嘗試下載 stm32f051 pack:

PS D:\project\USB\Daplink\my_dap\doc> pyocd pack -i STM32F051C8Tx
Downloading packs (press Control-C to cancel):
    Keil::STM32F0xx_DFP::2.0.0

等了很久,執行完上述命令後,嘗試燒錄,結果如下:

PS D:\project> pyocd flash -t stm32f051r8 .\stm32051_test.hex
0001009:CRITICAL:__main__:Failed to open CMSIS-Pack 'C:\Users\dell\AppData\Local\cmsis-pack-manager\cmsis-pack-manager\Keil\STM32F0xx_DFP\2.0.0.pack': File is not a zip file
Traceback (most recent call last):
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\target\pack\cmsis_pack.py", line 88, in __init__
    self._pack_file = zipfile.ZipFile(file_or_path, 'r')
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\zipfile.py", line 1200, in __init__
    self._RealGetContents()
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\zipfile.py", line 1267, in _RealGetContents
    raise BadZipFile("File is not a zip file")
zipfile.BadZipFile: File is not a zip file

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\__main__.py", line 343, in run
    self._COMMANDS[self._args.cmd](self)
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\__main__.py", line 470, in do_flash
    options=convert_session_options(self._args.options))
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\core\helpers.py", line 241, in session_with_chosen_probe
    return Session(probe, auto_open=auto_open, options=options, **kwargs)
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\core\session.py", line 175, in __init__
    or Board(self, self.options.get('target_override'))
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\board\board.py", line 48, in __init__
    pack_target.ManagedPacks.populate_target(target)
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\target\pack\pack_target.py", line 84, in populate_target
    targets = ManagedPacks.get_installed_targets()
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\target\pack\pack_target.py", line 71, in get_installed_targets
    pack = CmsisPack(pack_path)
  File "C:\Users\dell\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyocd-0.23.1.dev12-py3.7.egg\pyocd\target\pack\cmsis_pack.py", line 91, in __init__
    file_or_path, err)), err)
  File "<string>", line 3, in raise_from
pyocd.target.pack.cmsis_pack.MalformedCmsisPackError: Failed to open CMSIS-Pack 'C:\Users\dell\AppData\Local\cmsis-pack-manager\cmsis-pack-manager\Keil\STM32F0xx_DFP\2.0.0.pack': File is not a zip file

還是不能成功下載,從上述資訊來看應該是解析stm32f0的pack失敗,到目錄C:\Users\dell\AppData\Local\cmsis-pack-manager\cmsis-pack-manager\Keil\STM32F0xx_DFP\看了下,有檔案2.0.0.pack,20多兆。

手動從https://www.keil.com/dd2/Pack/#/eula-container下載了Keil.STM32F0xx_DFP.2.0.0.pack,有60多兆,對比下,問題是使用pyocd pack -i安裝的stm32f051的pack有問題。把自己下載的Keil.STM32F0xx_DFP.2.0.0.pack重新命名為2.0.0.pack,放到目錄C:\Users\dell\AppData\Local\cmsis-pack-manager\cmsis-pack-manager\Keil\STM32F0xx_DFP\,然後重新燒錄:

PS D:\project\USB\Daplink\my_dap\doc> pyocd flash -t stm32f051r8 .\stm32051_test.hex
[====================] 100%
0001733:INFO:loader:Erased 3072 bytes (3 sectors), programmed 3072 bytes (3 pages), skipped 0 bytes (0 pages) at 7.12 kB/s

板子執行起來了,下載成功。

還做了實驗,還是使用STM32F051板子,跑一個很簡單的程式,控制一個LED閃爍,試了下指定晶片跟不指定晶片有什麼區別,效果怎樣,如下:

從上面結果來看,如果沒指定晶片,雖然說從結果來看,是執行了擦除操作,可是板子還在執行,LED還在閃爍,指定了晶片後,執行完任務後,LED不再閃爍了。

(三)使用 pyocd python api

這裡嘗試下使用pyocd python api來操作微控制器。

環境是:

  • PC: windows10

  • python

  • 硬體

    偵錯程式:一塊跑Daplink V1的STM32F042最小系統

    目標板:一個STM32F042最小系統

    如下:

首先匯入庫:

from pyocd.probe.aggregator import DebugProbeAggregator
from pyocd.board.board import Board
from pyocd.core.helpers import ConnectHelper
from pyocd.core.target import Target
import logging
from pyocd.core.memory_map import MemoryType
from pyocd.core.coresight_target import CoreSightTarget
from pyocd.coresight.cortex_m import CortexM

按照 pyocd 的文件,首先是建立一個 Session ,如下:

session = ConnectHelper.session_with_chosen_probe(None, None)
session.open()

然後可以獲取到當前 session 中的 board,如下:

board = session.board
print("Board MSG:")
print("Board's name:%s" % board.name)
print("Board's description:%s" % board.description)
print("Board's target_type:%s" % board.target_type)
print("Board's unique_id:%s" % board.unique_id)
print("Board's test_binary:%s" % board.test_binary)
print("Unique ID: %s" % board.unique_id)

上述程式執行結果如下:

從上述資訊來看,是可以成功是別到我的偵錯程式,但是沒有正確顯示目標晶片,除了我要輸出的資訊外,還有一段內容,說的是當前目標類晶片型別型是 cortex_m,選這型別可以進行除錯但不能燒錄,可以通過 --target 或者 target_override 選項來指定目標晶片。

修改程式碼如下,通過 target_override 指定了我的目標晶片:

session = ConnectHelper.session_with_chosen_probe(target_override="stm32f042f4")
session.open()
board = session.board
print("Board MSG:")
print("Board's name:%s" % board.name)
print("Board's description:%s" % board.description)
print("Board's target_type:%s" % board.target_type)
print("Board's unique_id:%s" % board.unique_id)
print("Board's test_binary:%s" % board.test_binary)
print("Unique ID: %s" % board.unique_id)

執行結果為:

這回顯示了我的目標晶片。

成功了獲取到了偵錯程式的資訊,現在來試下獲取當前環境下目標晶片的資訊,程式碼如下:

target = board.target
print("Part number:%s" % target.part_number)
memory_map = target.get_memory_map()
ram_region = memory_map.get_default_region_of_type(MemoryType.RAM)
rom_region = memory_map.get_boot_memory()
print("menory map:")
print(memory_map)
print("ram_region:")
print(ram_region)
print("rom_region:")
print(rom_region)

執行結果為:

從上述結果來看,是可以成功獲取到目標晶片的 part number、Flash 跟 RAM 資訊。

通過檢視 pyocd 的原始碼,target 例項有個 irq_table 屬性,看下這個屬性會輸出什麼資訊:

print("Irq:")
print(target.irq_table)

輸出如下:

試下讀取微控制器一些暫存器:

print("pc reg: 0x%X" % target.read_core_register('pc'))
print("CPUID:0x%x" % target.read32(CortexM.CPUID))
print("device id:0x%x" % target.read32(0x40015800))
print("flash size:%x KB" % target.read32(0x1FFFF7CC))

執行結果如下:

  • 第一個讀取 PC 暫存器的值

    這個讀出來的值究竟對不對,沒去檢視,不過從這個值的大小來看(在0x8000000到‭8004000之前‬),應該是對的。

  • 第二個,讀取CPUID的值

    RM0091 Reference manual (stm32f0x2 參考手冊) 中關於CPU ID 部分如下:

stm32f042是arm M0 ,根據上述內容,該晶片的CPU ID為:

​ ARM V0 ARMv6-M M0 Revision

​ 0x41 0 c c20 0

也就是0x140cc200,跟讀取到的結果對的上,

  • 第三個讀取裝置ID

    RM0091 Reference manual (stm32f0x2 參考手冊) 中也有 裝置 ID 的內容,如下:

從文件來看,讀取出來的值也是對的,

  • 第四個,讀取Flash大小

目標 IC 是 STM32F042F4,Flash 實際大小是 16K,讀出來的也是 16K

完整程式碼為:

from pyocd.probe.aggregator import DebugProbeAggregator
from pyocd.board.board import Board
from pyocd.core.helpers import ConnectHelper
from pyocd.core.target import Target
import logging
from pyocd.core.memory_map import MemoryType
from pyocd.core.coresight_target import CoreSightTarget
from pyocd.coresight.cortex_m import CortexM
session = ConnectHelper.session_with_chosen_probe(target_override="stm32f042f4")
session.open()
board = session.board
print("Board MSG:")
print("Board's name:%s" % board.name)
print("Board's description:%s" % board.description)
print("Board's target_type:%s" % board.target_type)
print("Board's unique_id:%s" % board.unique_id)
print("Board's test_binary:%s" % board.test_binary)
print("Unique ID: %s" % board.unique_id)
target = board.target
print("Part number:%s" % target.part_number)
memory_map = target.get_memory_map()
ram_region = memory_map.get_default_region_of_type(MemoryType.RAM)
rom_region = memory_map.get_boot_memory()
print("menory map:")
print(memory_map)
print("ram_region:")
print(ram_region)
print("rom_region:")
print(rom_region)

print("Irq:")
print(target.irq_table)

print("pc reg: 0x%X" % target.read_core_register('pc'))
print("CPUID:0x%x" % target.read32(CortexM.CPUID))
print("device id:0x%x" % target.read32(0x40015800))
print("flash size:%d KB" % (target.read32(0x1FFFF7CC) & 0x0000ffff))

對於這段程式碼,我試著在建立 session 的時候,把指定目標晶片的型號跟實際使用的晶片型號設定成不一樣,

第一次看到跑的結果的時候,有點懵逼,不過想了下,合理。

參考:

api_examples:pyOCD\docs\api_examples.md,即pyocd原始碼目錄下docs/api_examples.md

architecture:pyOCD\pyOCD-0.25.0\docs\architecture.md

(四)可以用 pyocd 用來做什麼

還是隻關注 pyocd 燒錄的功能,在這個點上面,可以怎麼使用 pyocd 呢?

對於 pyocd 命令,當然是直接用來燒錄了,pyocd 是跨平臺的,不管使用的是 ST-Link、J-link、Daplink,由於一般情況下使用的都是固定的幾種 MCU,把 pyocd 命令 做成指令碼檔案,如 windows 的 bat、Linux Shell 指令碼,很方面使用。

對於使用 pyocd python api,可以使用 python 做個燒錄的上位機,對於使用 ST-Link 燒錄 ST MCU、J-link 有個j-flash 來說好像是多次一舉,不過對於一些使用場景,如對於不是做微控制器開發的,如硬體工程師、做上位機開發的純軟體工程師、或者其他不懂這方面的人來說,使用 j-flash 設定選項比較多可能一時用不起來,如果使用 pyocd python api 做個上位機一鍵燒錄,就可以解決這問題。

還有對於使用 daplink 的人來說,把 daplink 用於除錯是非常方便的,不用擔心什麼時候被識別為盜版的導致用不了而耽誤工作,由於 daplink 沒有對應的上位機,要使用 daplink 燒錄的話就不方便了,pyocd python api 做個上位機就可以解決這問題,如下是我用 pyocd + PyQT 做的一個上位機給一個軟體工程師用,只能燒錄 STM32F103C8,直接把軟體給過去就可以用起來,不用教這軟體的使用方法,這還可以做的跟簡單,一鍵燒錄。

還有一些場景,如,如果機器給到了客戶,要更新韌體的話,直接把韌體給客戶,怕客戶拿到了韌體,另外找個便宜點的供應商做個硬體,不再跟你買了,豈不虧大了,如果自己做個燒錄上位機,對給過去的韌體做些加密什麼的,客戶就無法用其他燒錄軟體燒錄這韌體了,還可以加個數量限制,就不怕客戶直接用這個來生成燒錄了,或者直接把韌體放到一個伺服器裡,只給個燒錄上位機就可以燒錄,原理上這也是可以實現的。