玩转 pyocd

(一) 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,直接把软件给过去就可以用起来,不用教这软件的使用方法,这还可以做的跟简单,一键烧录。

还有一些场景,如,如果机器给到了客户,要更新固件的话,直接把固件给客户,怕客户拿到了固件,另外找个便宜点的供应商做个硬件,不再跟你买了,岂不亏大了,如果自己做个烧录上位机,对给过去的固件做些加密什么的,客户就无法用其他烧录软件烧录这固件了,还可以加个数量限制,就不怕客户直接用这个来生成烧录了,或者直接把固件放到一个服务器里,只给个烧录上位机就可以烧录,原理上这也是可以实现的。

posted @ 2021-08-21 15:31  哈拎  阅读(9278)  评论(4编辑  收藏  举报