1. 首先我使用的Deepin15.6系统,我拿到的板子是绿色的原版

  2. 我们找到官网,进入其中的资源下载

  3. 我首先下载Kendryte KD233 Board Schematic V01原理图。

  4. 再下载一个Kendryte K210 Standalone SDK裸机sdk。

  5. 当然还需要一个工具链Kendryte OpenOCD for Ubuntu x86_64

  6. 以及交叉编译器RISC-V 64bit toolchain for Kendryte K210_ubuntu_amd64

安装交叉编译器

之前我们下载了RISC-V 64bit toolchain for Kendryte K210_ubuntu_amd64交叉编译器。现在我们首先需要对他进行安装。

➜  ~ cd ~/Downloads/         # 进入你下载到文件的目录
➜ Downloads sudo tar -xvf kendryte-toolchain.tar.gz -C /opt/ # 使用-C参数将解压出来文件放入/opt目录中,大家自己随意
➜ Downloads cd /opt/kendryte-toolchain/bin # 解压完成进入目录
➜ bin realpath . # 获取当前目录路径,为了添加环境变量
/opt/kendryte-toolchain/bin
➜ bin sudo vi /etc/profile # 编辑profile文件,添加环境变量
PATH="$PATH:/opt/kendryte-toolchain/bin" # 在文件末尾添加上编译器可执行文件目录
➜ bin source /etc/profile # 同步环境变量以生效
$ riscv64-unknown-elf- # 输入riscv64 之后按tab键 出现一大串说明是安装成功了
riscv64-unknown-elf-addr2line riscv64-unknown-elf-gcov-tool
riscv64-unknown-elf-ar riscv64-unknown-elf-gdb
riscv64-unknown-elf-as riscv64-unknown-elf-gprof
riscv64-unknown-elf-c++ riscv64-unknown-elf-ld
riscv64-unknown-elf-c++filt riscv64-unknown-elf-ld.bfd
riscv64-unknown-elf-cpp riscv64-unknown-elf-nm
riscv64-unknown-elf-elfedit riscv64-unknown-elf-objcopy
riscv64-unknown-elf-g++ riscv64-unknown-elf-objdump
riscv64-unknown-elf-gcc riscv64-unknown-elf-ranlib
riscv64-unknown-elf-gcc-7.2.0 riscv64-unknown-elf-readelf
riscv64-unknown-elf-gcc-ar riscv64-unknown-elf-run
riscv64-unknown-elf-gcc-nm riscv64-unknown-elf-size
riscv64-unknown-elf-gcc-ranlib riscv64-unknown-elf-strings
riscv64-unknown-elf-gcov riscv64-unknown-elf-strip
riscv64-unknown-elf-gcov-dump

$ riscv64-unknown-elf-gcc -v # deepin下执行也是有效果的
Using built-in specs.
COLLECT_GCC=riscv64-unknown-elf-gcc
COLLECT_LTO_WRAPPER=/opt/kendryte-toolchain/bin/../libexec/gcc/riscv64-unknown-elf/7.2.0/lto-wrapper
Target: riscv64-unknown-elf
Configured with: /opt/toolchain/riscv-gnu-toolchain/riscv-gcc/configure --target=riscv64-unknown-elf --prefix=/usr/local --disable-shared --disable-threads --enable-languages=c,c++ --with-system-zlib --enable-tls --with-newlib --with-sysroot=/usr/local/riscv64-unknown-elf --with-native-system-header-dir=/include --disable-libmudflap --disable-libssp --disable-libquadmath --disable-libgomp --disable-nls --src=.././riscv-gcc --enable-checking=yes --disable-multilib --with-abi=lp64d --with-arch=rv64imafdc 'CFLAGS_FOR_TARGET=-Os -mcmodel=medany'
Thread model: single
gcc version 7.2.0 (GCC)

安装openbcd调试

➜  ~ mkdir Kendryte_K210    # 首先我在家目录下建立了一个文件用于存放本开发版相关资料
➜ ~ cd Downloads # 进入下载目录
➜ Downloads tar -xvf kendryte-openocd-0.1.3-ubuntu64.tar.gz -C ~/Kendryte_K210/ # 解压openbcd到之前建立的文件夹
➜ Downloads cd ~/Kendryte_K210/kendryte-openocd/
➜ kendryte-openocd sudo apt install libusb-dev -y # 根据openbcd的readme文件,先安装libusb库
➜ kendryte-openocd ./bin/openocd -v # 运行此命令 输出两行如下,说明可以使用
Kendryte Open On-Chip Debugger For RISC-V v0.1.3 (20180912)
Licensed under GNU GPL v2

开始编译

  1. 首先我使用的vscode写代码,所以首先使用vscode打开工程目录

    ➜  ~ code Kendryte_K210/kendryte-standalone-sdk-0.5.0  # 启动vscode
    开启图片
  2. 配置c++插件

    可以看到我的图片里面有绿色波浪线,说明我的代码存在问题,这是由于没有配置好c++插件导致找不到头文件的问题。所以修改左上方的.vscode/c_cpp_properties为:

    {
    "configurations": [
    {
    "name": "Linux",
    "includePath": [
    "${workspaceFolder}/**",
    "${workspaceFolder}/lib/bsp/include",
    "${workspaceFolder}/lib/drivers/include",
    "/opt/kendryte-toolchain/riscv64-unknown-elf/include" //依据自己编译器安装位置修改
    ],
    "defines": [],
    "compilerPath": "/opt/kendryte-toolchain/bin/riscv64-unknown-elf-gcc",
    "cStandard": "c11",
    "cppStandard": "c++17",
    "intelliSenseMode": "clang-x64"
    }
    ],
    "version": 4
    }
    配置成功后如图,就没有错误信息了。 配置成功

  3. 创建build目录

    打开终端运行:

    ➜  kendryte-standalone-sdk-0.5.0 mkdir build
    ➜ kendryte-standalone-sdk-0.5.0 cd build
    配置成功

  4. 生成对应工程的Makefile 因为我们已经配置过了编译器的环境变量,所以现在我们使用如下命令生成Makefile

    ➜  build cmake .. -DPROJ=hello_world
    PROJ = hello_world
    -- Check for RISCV toolchain ...
    -- Using /opt/kendryte-toolchain/bin RISCV toolchain
    SOURCE_FILES=/home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/src/hello_world/main.c

    Project: hello_world LIST_FILE=/home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/cmake/executable.cmake TOOLCHAIN=/opt/kendryte-toolchain/bin
    KENDRYTE_IDE= BUILDING_SDK=yes
    CMAKE_BUILD_TYPE=Debug CMAKE_C_COMPILER=/opt/kendryte-toolchain/bin/riscv64-unknown-elf-gcc
    CMAKE_CXX_COMPILER=/opt/kendryte-toolchain/bin/riscv64-unknown-elf-g++
    CMAKE_LINKER=/opt/kendryte-toolchain/bin/riscv64-unknown-elf-ld
    CMAKE_OBJCOPY=/opt/kendryte-toolchain/bin/riscv64-unknown-elf-objcopy
    CMAKE_OBJDUMP=/opt/kendryte-toolchain/bin/riscv64-unknown-elf-objdump
    CMAKE_MAKE_PROGRAM=/usr/bin/make

    CMAKE_C_FLAGS= -mcmodel=medany -fno-common -ffunction-sections -fdata-sections -fstrict-volatile-bitfields -fno-zero-initialized-in-bss -Os -ggdb -std=gnu11 -Wno-pointer-to-int-cast -Wall -Werror=all -Wno-error=
    unused-function -Wno-error=unused-but-set-variable -Wno-error=unused-variable -Wno-error=deprecated-declarations -Wextra -Werror=frame-larger-than=65536 -Wno-unused-parameter -Wno-sign-compare -Wno-error=missing-b
    races -Wno-error=return-type -Wno-error=pointer-sign -Wno-missing-braces -Wno-strict-aliasing -Wno-implicit-fallthrough -Wno-missing-field-initializers -Wno-old-style-declaration
    CMAKE_CXX_FLAGS= -mcmodel=medany -fno-common -ffunction-sections -fdata-sections -fstrict-volatile-bitfields -fno-zero-initialized-in-bss -Os -ggdb -std=gnu++17 -Wall -Werror=all -Wno-error=unused-function -Wno-
    error=unused-but-set-variable -Wno-error=unused-variable -Wno-error=deprecated-declarations -Wextra -Werror=frame-larger-than=65536 -Wno-unused-parameter -Wno-sign-compare -Wno-error=missing-braces -Wno-error=retu
    rn-type -Wno-error=pointer-sign -Wno-missing-braces -Wno-strict-aliasing -Wno-implicit-fallthrough -Wno-missing-field-initializers
    LDFLAGS= -nostartfiles -static -Wl,--gc-sections -Wl,-static -Wl,--start-group -Wl,--whole-archive -Wl,--no-whole-archive -Wl,--end-group -Wl,-EL -T /home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/lds/kend
    ryte.ld
    CMAKE_BINARY_DIR=/home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/build
    Makefile created.
    出现Makefile created.说明我们已经可以开始编译咯

  5. 编译工程

    依旧是在那个目录下,执行make命令。

    ➜  build make
    [ 2%] Building C object lib/CMakeFiles/kendryte.dir/bsp/entry.c.obj
    [ 5%] Building C object lib/CMakeFiles/kendryte.dir/bsp/entry_user.c.obj
    [ 8%] Building C object lib/CMakeFiles/kendryte.dir/bsp/interrupt.c.obj
    [ 11%] Building C object lib/CMakeFiles/kendryte.dir/bsp/printf.c.obj
    [ 14%] Building C object lib/CMakeFiles/kendryte.dir/bsp/sleep.c.obj
    [ 17%] Building C object lib/CMakeFiles/kendryte.dir/bsp/syscalls.c.obj
    [ 20%] Building C object lib/CMakeFiles/kendryte.dir/drivers/aes.c.obj
    [ 23%] Building C object lib/CMakeFiles/kendryte.dir/drivers/clint.c.obj
    [ 26%] Building C object lib/CMakeFiles/kendryte.dir/drivers/common.c.obj
    [ 29%] Building C object lib/CMakeFiles/kendryte.dir/drivers/dmac.c.obj
    [ 32%] Building C object lib/CMakeFiles/kendryte.dir/drivers/dvp.c.obj
    [ 35%] Building C object lib/CMakeFiles/kendryte.dir/drivers/fft.c.obj
    In file included from /home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/lib/drivers/fft.c:15:0:
    /home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/lib/drivers/include/fft.h:222:29: error: unknown type name 'dmac_channel_number_t'
    void fft_complex_uint16_dma(dmac_channel_number_t dma_send_channel_num,
    ^~~~~~~~~~~~~~~~~~~~~
    /home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/lib/drivers/include/fft.h:223:29: error: unknown type name 'dmac_channel_number_t'
    dmac_channel_number_t dma_receive_channel_num,
    ^~~~~~~~~~~~~~~~~~~~~
    /home/zqh/Kendryte_K210/kendryte-standalone-sdk-0.5.0/lib/drivers/include/fft.h:225:52: error: unknown type name 'size_t'
    const uint64_t *input, size_t point_num,
    ^~~~~~
    make[2]: *** [lib/CMakeFiles/kendryte.dir/build.make:206:lib/CMakeFiles/kendryte.dir/drivers/fft.c.obj] 错误 1
    make[1]: *** [CMakeFiles/Makefile2:123:lib/CMakeFiles/kendryte.dir/all] 错误 2
    make: *** [Makefile:84:all] 错误 2
    这里我使用的sdk是kendryte-standalone-sdk-0.5.0,可能这个工程还是有点问题的。

  6. 问题解决

    这里看到错误提示为dmac_channel_number_t未定义,我进入fft.h查看了一下,发现这个文件可能少加了个头文件。所以在/kendryte-standalone-sdk-0.5.0/lib/drivers/include/fft.h中添加一行 #include "dmac.h"

    修改
  7. 编译成功

    现在重新运行make命令即可编译成功。 编译成功

烧录程序

此芯片暂时我只看到通过串口烧录程序的,并且由于我是linux系统,linux下串口烧录程序的文件在官网暂时没有。所以我在qq群中找到Python下载脚本。脚本内容放在最后,大家复制即可。 将脚本保存为isp_auto.revA.02.py后执行如下:

➜  build python3 isp_auto.revA.02.py -p /dev/ttyUSB0 -b 115200 hello_world.bin
[INFO] COM Port Selected Manually: /dev/ttyUSB0
[INFO] Selected Baudrate: 115200
[INFO] Trying to Enter the ISP Mode...

[INFO] Greeting Message Detected, Start Downloading ISP
Downloading ISP: |██████████████████████████████████████████████████| 100.0% Complete
[INFO] Booting From 0x80000000
[INFO] Wait For 1sec for ISP to Boot
[INFO] Boot to Flashmode Successfully
[INFO] Selected Flash: On-Board
Downloading Program: |██████████████████████████████████████████████████| 100.0% Complete
[INFO] Rebooting...
提示 如果是黑色板子,那么没有问题,但是我这个是绿色板子初代版本,自动下载电路没有完善,所以需要手动拉低boot。 我们需要将IO16的跳线帽拔掉,并且在靠近芯片那一端接一根线出来连接到GND,然后按一下RESET!!,进入isp模式,才可以下载程序。 如图所示: 手动下载

运行效果

现在我们打开串口调试助手,重启板子,就可以看到串口输出啦~~ 效果

附件

isp_auto.revA.02.py

#!env python3
import sys

BASH_TIPS = dict(NORMAL='\033[0m',BOLD='\033[1m',DIM='\033[2m',UNDERLINE='\033[4m',
DEFAULT='\033[39', RED='\033[31m', YELLOW='\033[33m', GREEN='\033[32m',
BG_DEFAULT='\033[49m', BG_WHITE='\033[107m')

ERROR_MSG = BASH_TIPS['RED']+BASH_TIPS['BOLD']+'[ERROR]'+BASH_TIPS['NORMAL']
WARN_MSG = BASH_TIPS['YELLOW']+BASH_TIPS['BOLD']+'[WARN]'+BASH_TIPS['NORMAL']
INFO_MSG = BASH_TIPS['GREEN']+BASH_TIPS['BOLD']+'[INFO]'+BASH_TIPS['NORMAL']

VID_LIST_FOR_AUTO_LOOKUP = "(1A86)|(0403)|(067B)|(10C4)"
# WCH FTDI PL CL
timeout = 0.5

class TimeoutError(Exception): pass

import time
import zlib

try:
import serial
import serial.tools.list_ports
except ImportError:
print(ERROR_MSG,'PySerial must be installed, run '+BASH_TIPS['GREEN']+'`pip3 install pyserial`',BASH_TIPS['DEFAULT'])
sys.exit(1)

try:
if sys.platform != 'win32':
from Crypto.Cipher import AES
except ImportError:
print(ERROR_MSG,'Crypto must be installed, run '+BASH_TIPS['GREEN']+'`pip3 install crypto`',BASH_TIPS['DEFAULT'])
sys.exit(1)

import struct
from enum import Enum
import binascii
import hashlib
import argparse

import time, math

ISP_PROG = '78daedbc7b5c54d7d53fbccf7dc01b7ad4c14064e228189b5a74504824664c44d45c6a9378499b54c901d104af68306d6cc19961444da239ea60b0959828d6b6a9e9a8636312b40d62da3e4fcd45316d1ad101062f091019060d30ef5afb9ce17262f3f6f9bdcf1feffb793b7cbeacb3d7d997b5d7de7bedb5cfd967af2085a4e9bddf7e9e3f2739397f8e056047a4785842d40dec80fcad8f26ab841099813040463e40e68002641e284016800264112840968002641350801c051420470305c8fd8002e4fe4001f200a000792050803c0828e0faa89864e5140bf95b1e5e31c7fe90671bf08b8afe6a7e056559dd28af663bf2c7d85b54c142949547883c7c186363ff08f20e23f2e0c18c8d994656d80b736486a9b5a60c26d6b43b88357d02b1264f63ad293f60ad6959ac357d156b4d2ee2ad29db786bda6bbc35fd2dde9a7c52b4a67c285ad32e8ad6f416d19acc4441fa28481f05e9a3207d3f48df0fd2f783f4fd20fd00483f00d20f80f40320fd20483f08d20f82f48392929998a494c131496977c424a54f88494a9e362429e5074392d2b28624a5af1a92945c34342965dbd0a4b4d78626a5bf353429f9e4f0a4940f8727a55d1c9e94de321cd28f80f42320fd08483f02d2c741fa38481f07e9e320fded90fe76487f3ba4bf1dd22740fa04489f00e913f2c75852ae8f8949914506da878951e609a4c97e62ed0a6cff3931d1f931c3521cc945849bc0308e09450c3791611d138b58cec6700e5b11c7a530bc23a588e7263182635291c04d6644c7e422914b6524476a91c4a53126475a9189bb9b8972dc5d14c5ddc3443bee298ae6a630fd1c538afa71e94c7f477a517fee5e6680e3dea201dc5466a0636ad140ee3e6690e3bea24112e8c79d5c14234d6006bb27140d96263243dc138b8648364676db8a64298519ea4e291a2a4d6286b927150d932633c3dd938b864ba98cd99d5a6496d29858775a51ac743733c27d77d108e91ee636f73d45b749539838f794a238299d8977a717c54bf732b7bbef2dba5d9aca8c744f2d1a29ddc724b8ef2b4a80fe61c9279664a061ec27d713ed2d1e37f43577f558874f22a96238cc55b8195b4135491d8efdbcf8635b6e35b1fe2640804fe45c33b1be81d7d5daf57eb8deef26a9d28db02c85c2b6e71ae07e35f0aa89ada041bb5f91c9d0b80723d749fab5c4c80581b0f5807e8d7cb896a518a26c6d606521863459ced8aee74e4fce277362ae27d953d4909bf837495d4d24e687d88e381ea12e4c6e2121e68d8470334fdf2e4b2bc914b3c4e8f51ae901bee3f4e991b22999601da7c4c632b65c89899324622d0f9038775bd8ba572b17eb264bcd83ca5cc1b01a0c0e524aeb79ca07b91cfb32d9329700f105a2b41e052a822d10c91457955dc9ab67d56112ab9e15d8e3c50d769b7894f84bc50eeb0128e30d89ca8932723305b81e06e54ad28ae60b37b87d92a8ba8691d75fded7b5a279cfd75a1d360e8ed44135c510ad1e9943b47b3387d03a949afad4c1fa8699e923f7f67ad1b14f62c13eb1c7059f5dbe95cccfd4f3bd65c67aa2dcb40d04c22aaf88bc2adab1dd59b9603ef1c7d7747d7b7d16b6635db8aa60d85a5e2162fd50062c738ae9885d59739ec44926adfc8da8b34612291fcb4519208d96c769cc03f3dcd309eddba54a8ba09c22cde6a2ed85be614d073bc7b790a6e60bd7a02f307a5f60512e4d675218751527f56d5f47a200f2d6888ec40c4a69dbee87b62d16a97e9435e7200de8a958003d1db52bd9d0b6c341ce73223b456cb07b5d67893ff6166d8b6d593c1cda723fb66507c8d2694ddf06f3437358e5514f6718b09f608f07b3d6a12dc49c8df3caf4734dc9858b9457f8185958499a924f3c290bcd40ed0be93c236cc5eb25aa7002ae0b713c4c83ba33946f89816b3bf0cbe17a8e5d15e6c0f521b8de7a9f2aa0be4e60fca92ae40b6563fca98e549e984137d67b42d760de62e8351fbaa24a851a9f45fe9c6ebea647f715d4a32c59681be0d8b5ee97d848fd3d128d3388a699186ac036d278d27cadad2c5ade69a1baa6e6848fa0ad58b0b32cd8d9489b71a83f5a8fe4130fc11cc8409dbfafd5ff04d67f8123d5a9c93435548b325937499fab52b996af29540bfd8381bed38a7dc59a06fd44025da0ce45b4113817c4f4f419827d66e1bb8e4ccd7e58492bc1fe03323191fe83f9c8420b8d0ffdeb2697d142e3c86b87915e713a55d089b57f0b299b2f30af53ea22b47d693bbf05f3ef49621dd0425e1ff01ae4b3e7639853886332837a6464e85fd87e90ff09998f41da1e2708c4dfdfd515d1511ffdd0714058ecf3b43c5e2f0feb04b2be2e60190bffabef38a1f9bf6d1c1f7a5e62af31f481717c415ebe5ba4fb5af3212670e01b70d6f41f30e01b30d6945580c1e0536c035fe30e4e8d02dd6f02f022fa4131e007b19ecd90efe607df576ee359c897d7e75a0ee6520ee65216e65216e65206e65206e6d2886d17b05c357463d071f74db021add0b63c2973820de45bed72090fe3b50cfd2ed6e1eb07724713d91545e2c428122894886a8e62d51a893dee1c3ded78d4dd8c2dfa26f1efe43ba0af8420ef9bd6b4699cfcaa883e12d4278b83bec3c83fbcc2989f449fadff63a9df21c4371bfb2ed439f90e017c22a833d4357d30f5a3681f2b81768c827a6e16892d7a1313d72f6e54d9a64d7728bba3b09e02d453a03ec5dd50dfbba1bef7407def81fa4e81fa4e81faa633111d8bb4aed0c665ce28b0d7a90cf89c2cf665a5b64070244513afdb0d731dd44b4a265a3fe0f57ee0d4fb00b461310f6d184534db5084fdee25d5c3533d4ce1b7135bd40d72bc6417d8da8436553a74cb7cd066615e56b1a5577e7308a4f91ad270d6d12dba8dad3ee0382ad171c865ba0fe07838ee09107331846788fbb479ac9a780311de8c7d6887233c6b62112973e31cd6067665de2019ca5285285dee0baf831d6d85f2fa61796566902d31526ee66b3de54aaff52db7f817df2cf7d42f7a971b2953956246590fb60d8472daa11cb1a75e9965dc51cde661d8515dfdaac705b4aacaa38a2bc97173881cdf7914f2aa211ec8df71ea94678ab9a66f9d2c50a771f306415ffb0af21ef8cd3a483bfa96e1deae95e1da76eb328ab745cae8c97f31e67f0ded1bb4cdc7a80f5992b638264bd43ef9d03ec21c046312c6e704c62b05bbed13f707377154880cce4d6a14f4a9500df8626d74dc392a6630653566e09989f28a644a75a24d789ff1ba0e93267262699904fec45f25d1eb76a27fc0948966229bc1df980ff3c26f6a884dba0c76b502e65537936aa63231ca733538070f047f85e57c515a3949e0e304c0ef1b1b22bde705182b0c8c956edbc7ed9b41e4b541a8eb9e3659aa0dfbb7063a68d947c1f7ccc4fc33a19f4e2736771d51469e23d663e0b31e0e10acbfd7dd0afeab44d0b7b01e83f6395c03d77bba341b183380da409833704ec6f1dc44ec7370cea27d909c784c1678e43d5726e01c8ed7858f3611f2c82de68cb0157d523ad727a39e71ee1a3d65ae8bf514d0397235f5b125f0b1df001f7b4d1d891382617998c0c97305ce96f306c85941e8fa2485e9f66b1dd5e00f55431b4127d1da3ef095ba53e2c01fe164b40d4b6ac69ae7a30edc51aa2711ea0bfe4c85d6fe741eea6e7f6877ec03705f05bb2c3b414eb0d3684fb8aa1d508e8bfa48f23017b1e50421bc0fca94185b3dc8547d04cb67bc810adaf794e7426883ba56107b7f39181ad4d4fcfd164712d8086c4bcb0db46170af30da9104728792d067ef80f15c0ab668139de3e205f0912e74f6ba7fd3513d17fa7806d4b54aaf6b0d8473885cec22ea4e11ca6e83f05ab82e66bc0d705d7d96a0cf16d10b575d8f7d90f1e686e03a487cae635fa82e61a26c065d032f4e7253bba6c4822f571d0a2b856d22d88daed70b5bbfbdef6588c45b5d41fdafb8f3da98382e35dbbda120f0c1078736449df887085db2381cca0985a9ef9bb99d70b39cba7c490c37eb0d228f8802f96e00ff305c9730b6a537485c14e8cd3c8b539c922603c8d25b0ef35cf48332be407920df4f5283e1b06dedafc03e0e0aab51384f7f3fa8f90d93c28ecc4fc0d6bd0f639567b472612d36a30ed2c3781b0e3acc6d83702b5c1733b6c56d304f8a68673780eda8e7409f0ed03de88ba0be1ca04fdafea05b07e8536b7fcdf6c8929d28c501b49fd08e09d7b87d0203bedd191857b5a8b35447380cf2de803e27421f043f039f39c0fc993241d0eb73d1c664e1f8bacf57827d9761529de1b0ea8ee2b4fee9ecf43a3d2415ef89252cf5055d74ec8d83753d2c96a3189b5403636abaae9faa7f5a934398df3dda58ab9e82630dfd185847b2da5ab3e61b6bcd26cb893bf17e93858cc6b9c783cf775cc26f34de89713eb05d50dfc2eb1599c9d773f964ad2cd7a7600f5250e688bc5e77d2346b0acc95109679be13797294a5d3e674531b04639fc1f8746c1e81f1b35510753e1b19affe2d524784d71d17f9855207975a44349bc493d74d211acf6a4aeaec1d17d64b68a706407bdc7080bdc034a83359e4d147ee78dd846370e1719f5bd337ca4475ade76b3d44d34751df4820fd572417763f278075647f65643dae2f06342593ef815f3e80aebfc146ad4826e365e9d0007f6c7d17d8bbefc25a036458d8a5ee10586a63617d47ed6bca36eaaba27df4f2478812bfbddbef86fedeed63721920333f14fdcf76b411d4a7471b9df21af8d36f21bfcd318921dc24ea23dca03e2aa4a5fe3fc8e9a880b969483dab94a29f043e7f7990809d62ade5606b60ee905d125dafd1e70c9ba0fea285281b6a58b40d2877995960b03e6535d234ff6df51de85b40dee0f79c18a0c4c2fc595e8ff6ad3faed755f1cc004ce71f7ef6a6c63f84fabf79ef03e3dccaaa3fb16a41889cf4bf7b517d4e0cdfbbe9ed9dfdfdfd03d324ffb29aaec19736147ac53fd93789f80c72e7cedcc2fdc42af9c8eba20fbc2ea00250c0eb3c50c0eb1c50c0eb2c50c0eb0c50c0eb0428604321aefbd59d2e72f64575a7c0d0ffecd9179ff0a93b93885646f1864061e2cd9d8527cdb7bf3c75a77fd59fda7fe1bb7dd5dec671cef1aef175c5d79a48ade4c9c51e18b8f15ead2bf3bdbaf8d8bd3953af95644e3d933e735c87208c6f1fd7da7fc9b6c02888df442cf2de1c8707b4e549e294872552b298aba88a3efbb2b534486e14749a6fbc6cdd1f8c4e368d7241cea33ae70fbef8abcc864533b2a7bac6646d28d4c769d741d74665457365e8a0db2571609797856dae0afbfa611b0a990c88350c4764c6a815a4fcb1698347d58a75e39cd6d823c43aa20a7a03ace453008942b4351da8057ada6881bd634651065898e8a6e6ca80c74cbdb9cef80eed4a22e9b5da959b4fafd3ae7225d29edeb877119b3d8dd14a650627d6c13aa85f993318f63adf6767d78e2f19d5285e535dcefe50971f8c7332ff454b1f3d86c9124c8d603bfa15654079b5a35c9716dd9fbd379b5d2a74d7516ac61aeeb99e3eebb5abd3068b75a36ab5f571c557e95fbc98691d1d7c54955c8f41bdd2828f42ae8f9a729819db20b7fb5a4c4b8419fbe06a4fb3292f6ac60770156e7265a69f8ec88a92ea7abc9c58e75466d78ad7de9939aa715c495120d10516f986eae41f2d5cb27b7a67aa724e22bb1795fcc5b9a8aa9233f3cc806ab054fdea7ddb326663cc968db5eb5bdecac4d99b99deb716270b5c49d6443eda9a0c56228527f1a7ade97065e141dbbcaeede9a0ed0b6780cf031fec3dc0c29b722a5146fcd3a40c5cd8a8683676df251893c9ba5ff47937b77105a91d1fff4c51a02873aa6b43a1a3466046acdadbfee7e68f1a8b3bf6b7fef7b573757faffd71c7d3ed8b5b9f695e7eedb5cd13378d778e2bf9f06fbb979dfc9b0356318ebb6105081e05ccf2035d9b8b66c3ac3a9cc9287a90795031f51b3de2f20f8b7f95f163e88f5ba31549182d08c7a3d74e9bb745c9ff11f3c32da645f17943e7285d15a3a64f3fb25bf9bbd3fab92bb1b889cc1916bf68af6bb66bbf27beb629c63e421922c41cdff4aa5dc8503cd104ef34c5d49ad7d73b52a399fd65b67e657698bbecd6b457c987adbbcae97d7226bb73fe980fbf7bc0f17614711c2e2127a33acfef2f56f2cbc8df8bd317f55f579438af78b66b834779bc3c1af339f86ac9161c07d6b483e4f91b45990d2b67acd9bf467cbe61e98c67f63f233e7be9a7f7bfb0f70576fda565535d51d046a9fd4861d1eca2e9f283fd4e3ce5c352b877b194e9d3f7bbfd5103ba38d08d6991fa9c6451368a0395a5bf30c1f5cc0f4ef173fae71de777b3035e2c1b00f6d17d3bb9e239fefe764e09b61261cc717e1fcc9b927dd6dff6d35ad8470dabd7fa08ea5a0d48bc35b69574ce7fd187e5ab0f5e0d330fcb0f7e1d1ef050c9434ff8fae72d2f1e077662d15a2c7d6fae5c10f2c9b992c91a5b47e2aff167bef41c771e9e165f3bfdfd2bee2b4e25bf935c719b164d2d3ee79a0aa96ad72f98be37f7f63d0bde8f5df436689e6af755db805f746b774773fd91c4622ad7cf0fba9f0af6cfa32d3198ca160a8550b692caf83c053c5868d7827fb24afe2fc81567229400f9173c397d8adb638f5fb4e07d6cd98ceb5a4bcdf9e9139f0ef0fdddadf87fcd5c711fafae61d3a07db0758e17d7b0ace7a0d3ff60791be3731c071d1f2f214cc6c9a8b9deb2e83892735c79f03382adf963da9610f3e1f2cf3e7df7e9e2cfb1bc954f4ebfe43cee764f7bf2fdf84597a0c4b5c79f5ab5b736b1e3d1e679d78aeb66b73fd20a6bb14799e9c2ecce1fee6a1ce5dabf48ccbeb4e6fee7f73ecffee4d23359d0cadc1fa087ffca493aef563e8922ce45972b1df3a318e5859bc461be935136ce67668365deb0636fceac2dcc68392aaadfa7953d65fcb0f1c7755a39e336bdb9492febc1ce1f955c339675ffb37b9fbd55799f6ae5fdf46bad3c7701f367d7a3ae0daf464abb08a52576cc6e2faedb5bfb486ba4cca76b55e75a06e65e765c89d2e599038e59e1c952ea83d77988f2d56156b73b19df9482cd4339407b8b5469658272d9c3ab3529a42829ce6d26716633f12c06bb51dc601e284df6905c5594c2caf04f08595192a4486e313e3b366fe89296b2bd9ed22f875e2dcdc5f710a5016c65f224f83cccb44ab9663bae0a38e62fdb329540c0a4d66483c7e7e622f90f94b4124e0da6f90b52787d86127598c466af2ab3891ebb32a72aba33d595e4590ab14a960ee20e47b15a8afd838eba94e84f06c666afcfd0e3cead32792528af6e28e1eb3c39e0c31d0d0ea4f4bd60ffed4ddbafaa8b8793ed81519e18ff5b951f662af5e06d4b3e6147a52ad905908ea835efe3fa4f7cd1079c611aa71538055251a52adae7828e25bfbfa155ad19cd94b9aba5ef560ecd53668a247ec9f6a6dd57bc521d09be3bd959e43363996967454a93cf8a186794273e570d98497ca0ea882a362728171b860f70304971f392ecae19de9da78890c41d2dd1b5f2e14b8f78cc504feba47364a82283de63ae0d6d0467f5cbb89a24bbf0804df6130e5c7be5e95cd1fb5c2ed9d5d052267278b52da9fef3f58ff4e8779af911cf3d1ba7bab8948d8c12cb8f10c6a8cf4d22d34cf537d5b654a2ec16e3311f35f37af8c6cbe65c7c9e116a77801c22a7d42d26ca082e9e3b1e055ee46622ffe4fdb092fb2af14aafdad7672a0f86c6eaf34b61a1a2f935b9ebb62bebe7276e9fba45d9d3100bf31fab8df9da82cef907372942d0e2951e64943541a2244c49f03c8b4f2cbff333aea21fb9630fb6aaf560c30b4ae97cb3ba339353b68bc31dbe28e6d1ed72f650521840d96429f37907c48e5570854ca59d105aa7dd492a00ee9ad06d943b31f4dcaa83f23c0fd93643b9748a57e7e5c25ab6988b2b86f5e3f0e164a0f8c84e5910c3314f2b23aa4421b3ec5ca2dd3bbc866af4d397680e77d7349aaec53492cbe4cbe78f7c3843a93f45cac4a3c27aec07d0574e11755e35e4d920aef5016798c60901e7396969a52c405f11a1af2ca96a57e7253165c5a7a41ff8629e51c42a327471cbabfe676bba4ccb337c43b363f25a5ef56edc69577e34928b5dd2d37b639a62af503926852e517a6fa8560ec4922d4d5bae9606f6eeb41e1ace64fd41f98b89ecddb97b31f6abdd0df14bb636c55f99f5d644e71defe2488d553a53fd036b6a07bcbb2d49e46ca1c524e742bdf7a0fb9f95aedc4e69eaf6ce82f55b5c47b8c9fd49e73c65f83913b6a1f5adf94fa9c19ba4a4ea64a6d7e5b3bff9aa2bf7bd1df3b6779ad76fb1395d76bd3d7fd8395f191a24f35ccc746c0f9b3077da6b7bc66da7928e0d2dc216a3b9bd3dffacf5f026e267a7d4438fc9cbe45c99d6c3604da15d4b735795792518b5391e02f7a68778ccb930f657ef79dd37ed7fd843f31a157af2c54a472af43d7e53d7b452d7e6932f5b53be26fb6678a5cd3092b75ff5af757fae0afd183ab38c6d390ef681910343096a24b66eaf2651aae7afbbeb7e7b149ffcfaaffade04eb34049ffc8e780f7dfa713b95a15504bd524726acf25ff4750942a7cf5bbd8ffc62b34daa2245815f642a4dd5e4a05b595a0feb00c72981b82497f0facb355dfe2b812e581914ac25ef5e3de8569f7387e562b143e9a8161d5e81387eef228e4a81e12a5d8ce33d81e5de73b19c57e01cc75c9cea72f1e82373c75c61eb3b0537ad87e677580f873aadc7029dd6776a3aad87aac3d6c3b95dd66366b82f85d7fa7a97abb61590fa9bd32ab12db766bd26bd2658dfa9eef25f05890281b60d8528b7d2b00f6556032cac84dc55e4f697fccf07bbbcc5d5641bac8d3acd8e5312596fb6921a12f4b904c7a960d82561de377ce84fe2baa5f8da78e7feba3fd73ed2f168fbbcd61f36fff8daef5d51d76664372cda9f2d2ebdf4fcfd3fd9fb13f6a7979e1d9305eb8b9d07374dac6b6afe7907ac59263bc3381ba925302bf5e3c3f28d5361e5662a87f392fc755498ce4cadb91ccc4ae53709331bd72c8fba8c796e28c4b71afee5c73b94c797dab4319efbbb9867bdeeef40df514c6d445e1b084fdd61ca114a665d86be1475992823db5859e8c72a7ffa95e9ef65de92a5d3682ff415fc56bd3c82580f4f01ffd5199ee71ab549bd3182782f2f65ace929615c091ca9543e582a94aed8fd6c6953fc55ceb799788b974e5b3f43d8ec754f6194b5e7c679975ea539785dcf326a03cc23f5a5b92785792e6dce6b222b6134c04ae29abca91f73af8fc92cca746d523c62bc238587354e999d7bdb0916f4cfe1d9af2a2f4943bc5199608ddba25509a43d756348e992dd79b258129637f14c7c53e955592ae415a5daa48a2bd98fdc4ae72912bf6877637cf6ee6b304f73cac56a7e1af41f9e8d5d54ea8fcd2e0dc42e29bdea7567335867dd5eecd6ea9ccaecce766df65ece65ac8753189471e1a5e78fc42fd9dd1c9fb7bb55911a08da69c88b9b5b19bf12aeb9dded5c2ab4a09be74b3b327c7bc10381f5cb3f1ef6297f5c1a851aa272ba7806358572a3bc2acafb65352f4b2b59657d3501b91a41ae6b324abbac9a4ce321051bbf28fe627c767c7dfc92f8cbfb372f7d17a46e8ecd2b0529daa81410876b41295c3caf49e2004976775c042960fdf3518b4f79e41969775ee992880cff67657f5432cbf73f2b3be12f2d3ed4ddf71ac76df20f14aee03d5912c237ca3ef728a501e2b8db15566f7c10fef3ae0fbd281b95a9ae5ad465e26317c55e8ccd8eada7326555b3b2043eb424304395edfea1b9db03b1cf965eddeb9a87b6f5476f815e622fc7e6c57e416593a0752481fd146593782ebe65e81a087343831ffa0eba47b9d75752f94e7ee0fb73c96b5eefe5cbb497467cb1235e5cd5e313089cabadfba5ed625d53f2ca68b0df2382449fbfd5dd0f6cfb53fc89c8aa17538c773a768c61202e69e69559027972c941cf93dd6b785c01ac2f588f4f20600df0858f5a4f584b3ebf6a5473e2b5d98d8fd489ad6a488a39e81cef9c5872b4645735e65632839901b637af6f6ac7b0d10cbb66efcaff2e9e5dbc77e9fd4b135d97b233b260f5caf795a37049e77c5ce7287141d2a93dfd80d411895cb99fd2f0ccc64e1fa65841ca07476afe7bd7e4625afb0ac9496bcff5ae7d85a36fed61c54f7d99a4ab8975079d1b157c2e31de1955953e6b97bfa9367c03d7f6aa59e24eaa6a9b9bfcea4bb54d624abedc97b9fb04acbd785c7bad9fabadee716dbf6df38eeabbf2b67da04ad325c5152489d0c227eec5753c6a01d6f7440df93a51f2f83c5883d1559f9b68e192260ccb12b1802ec7e2f3e0bd2e4c5f9e45f9213311aae3e7581379fabc3cfe93f83c26e3d3eb0e33de934c932e44d68faeca92cd4ce6367fec9c05efb75476f707b4126f4aab512396f9ba4672b5f728468d88d7463526d6cdae2db9ccccc2bacd76459e5d600b4774735f23967de3822ea598be641cb47cff7589b8ee7e4c20eb9fd0568e92a9a4ba7f1ec6b17cd7a81b7217eae6ae17b655d1fbdfc1271d4db5175aacb13c69aafd79cb2c9f2663e687da73ad9fb7e11332ec77b247e2a6a989d7c456ec7f301713944dd92ac608b9b224157efa7269063e39531eae25c79e5066d7c29cbf7725bbe652f6fd4bf72ecd80baa465afafe17e052db23395b396f3a4ff3ae5bc689ab304a5deb6795775fcf4db5f4acfdbf5a7f40540b3fbaf2b9a8135eb9cef9807bdf2f620d96d5f9ba93c1d32a54a768b20d9e68788b079d25f3a0b5477e8e7b179c197cb3c12834f3d636bb155b0ed654f2aa72c9088e64dedb66ff3cf6cbc2bcf5a3e9a39f2616fad94df865a29aa6466b8a04c16ca1c57ec1f166cdb6d3ff23194e90a90273295ed011e537ed7a70644daeeebe70a3ec71f5c8c7f4b5433f707810cd86c3d3489991275f7b4a2ccc1fedd3f78f27dff424b13aea7ff5cfc48316da5ccf2ee270feb2b0fee9a12f52b7b4b253318c7c038e70868e792c0f8baa6da3d1f684f2677558f72dd9f7d6991801e40007b0693417b858bf9426fa7a391674f42c78ae6152feaeb8423f499e4cdbb578d6a4f6c9dddfcc8b5f17562c7b812cf13b8ca8b9ad0d4dc14421f716f8e33d7952b3b439d2da503b6246e9f50b87ee8445e79aa95e033b5bf05998c4b2bef5fb3770dfbfca5a57740b945dab3b6b730ed7e8f7f64b0c32b14da9177e177a0e3b820abc508bfc9418c4ef022ebedfe7fb4ddd45694c5bfd9feb4b6421bf6225d8dbcbeff705625e655b405bd726740935ef2ee7e60b66b8ad36da763a8dcfdfba6e6415f08193b2a954261246a2bb16e9cf34da778ada979c555c711984b8bf9b0fe5ec6aa86eac3b6e23612bb984b131857c9aa3265c469fe6d4fc965cec78777bdb4f599b73da5cb63bf8cb92a832ca50df8eed13ab9c6a27940d57b61957325a08fd4c0812dca28a84fe555ad1dd0e6ac68fe7e5d6c033373c745c7113eec5900f13636c6bff8d29b9e2dcf289b8645398e1533d6778631d663c7885768205b725c253beadff6c43ea3dc6e123d73b1adea4728669f689e47d79fb7517aefb9d8d22f4baf083335a96cee3612eff70f33b5cb12ac65b343bc5c934af4d56ca6d7534d62723e7c99a64c691b5a26994999c74c5e920e7a4a15b9580a6f85bc60e66b7acd27ccfc6d65cc725cef3ce593cf79c8e04c981f21b75c7ca3c8c589667dc5366ea7aaadd84aab445766dcfc24bbcd1320f1394fbdec410dbd1518b8e5cbad57b65f1efae5d9777f90a9e45443faa3c2499f8c6bfbdc6a22d7202720ae47ce308d836f2d0ba455952aacd7645cdb5fae6a976b929838a95a7a02d76bc555d04eabcafc57035dfd61bda6cd8ddd730ab6fe6fa5916841ed837a5bd0dc917d2de8a45589f56fd2996cd465f18b894ee533581dd480fd2f1709dac31de0c310fb8003c9a6c9f8dc7ff0dec5fe6942d75799f7e60a39375e8ecaddf5b2ea8315de1f31567cad2c08857796c3351f5fcb645c59343bfb60f680ac9e19586cc539b864f37867c9df54698d145f1b7fcd95197f2662edd0d63d4e92c13e82bfdab637675c09d898da3d7f67bed0e70ecbf35fb794d177cd68452a3d017c7358bdfe9d6b681b4cb5a66b601f4c74f6cd8d3ff34ae6b6bfe11c4e673613f301c6c1d4669a1a43f816d61a3b9a99e6fb86f6de94a2417b96953feb33ff447fd31fd152d1346f48264c13b3ba779a4ce9d669688a5f4b3ca648ceef9d2289ef9b02df6529972a58d53c86a40a84b5094142df9f634d2cc1ee7d88b2d01cd6f61141fc2b15ac3c6c0c81950deb7557e0bb4f6d5f5581992803253d1c43e83edcf2409ff474ff40642f9bbe87207fb4bd25ff4e7b32788fa6eb3eed9d28ac04589b54c7622f875658a13a2562c3779793dd8cb24924dc2489ee850b148a04f7bf5d2fe0937d49f4bdeac6eb1552727ecc2ccc8ff54aad2ceed5806518a3c448bcc3e726ca16894dcd24ac3c3485d8f83ac2a5f2747f8335f99befec6597d0fdce1efa1e95affbfda1feee91ee0d740a04eb71bd60ba263feee9685ef8852e7f963c743ac1f7b6b8575229061d4d72d2bd6446b9715f72f71e10174f7abfc3c47d7274bf67ca60be896cbd5f1609afed6764c04bf8fe7bb8c7ce6cc3ef0b1e68934d24469e3fcfae0e4f24eab024909de1bde2c7c4ebf2e1fe1ce2382074c86bdb98b2736e466d1b4e8eefdc4fbc0dc3c1ef023facfc6c876c4ec2bdeebccddd401c895247598d87a17b143c15740f85d512ea88138713ff48f106e44dbf594815be3347160b09eecf55872711af7816f23a3a10c26071ccd85f31dcdf1f538d7b11f93e7bf1a4665669a888c5fdc6743f459e6f38beeb55cfd6331ea893eae02e7b1d5ec89b23d8f7e2ceb988fc5c1b291b524cb8531c293b5745b01eb6c5f388ef2b18b91bbe0a97ed3c456ccfcd03de395276e12cf135003f9c00735e5b38ee423df13e07fc3d1788ada18d519f4b20717b1a88ef14c4c1fb35417a3fb5381c96c5b6b0b7a181c1fe1d670e116fdb29d0550329fb5c6070df8ddc3692e03ef5b8cf873171c345c6166a20dec560690b46821537439c2eba8fbdac6612bde713db6919b8071e755ae649656c0535747f44dcc7198c0ff7f63e2783be20cdf9b94cd9ce198cb720446ca07735144bca76ce8738b561bc8ffcb8f3390cb6894f3c11960b62b5fa9973196f48dfd775762dc85048655083c3883704bc1dc8fbce1c6fc0476c42051debf49b13fd3db6fede3a4cdba5ae22ba7bdf0f8edd5e7bbfacc9d3e81e71dcf7a3b71b0fe34ec26f56940d411ef781a5467f678e1a45b83801f7a48c215ee926dd83a08642839452188fe20ea20a8512f82b17e2a246107ffcdd9d2a8c1fba37a379cfe728d7adf6d270f73054de9ef1d9dcfd1efe7a92bd45764bbc4f24fd657301c17e6cb5405c58fe788b1b482a8c39dceb611d25814f2ad13daaa912e9af0a31bcbc3391d8c46a882f92b262e8e3a547bbd49dd534be4fb440bf8674388789e47eafbb86f5cca37ed438b8c6f7fe3cee79b01e96206d1bb491c4f84bc52e4ca74a7344ccc3919849f71829803268437fa9d4d57b4f48c4e682ad85d574058f75899b3fb77b2f9c0a46c52b8588ea49c27af447db853617f7eb4dd9b19fd8722a08eeddb19657dcca7ef34a2dda6f8105fbdd5f9e5bcfd85cd8fe47be1197ead02cb0b260e1210e2d5bb7677136a11ec66106a169938f80bf53d59d1ebfa9903d999cf2b83410daa6036c24eed580beb30decfe1d9c357d0283795f079b4fdb673be8d6590cfab7808eeb88637426e1ee04fd39a10fece459799e08f22642bbed278efd22dddb84fab35a70ef5fa1293f3533399fce19161e74c1eafbd6067bdd30be3c995457f8184e8906bb7b04ec1bce1b44a2f3860fcbe4615d326afb37e790adb392ad96d10cf43dfc868783bec742df63a1ef45e6059eeacb25e8ed55cb806e87a26ecda9d46fbd40f50bbaa2732a8e3d7780ee8d4b95a6cf7124499c1a0a84bdf53e621deba3365ee367f6f0c7f5e597ed1846f754e29e921ede185c4f032fa1b38737097805b867eceb1e5e068e39fc8ee0660f6f2ef23a21bf1b3dbc1ce47d0df9b5f7f0d6222f04f9b5f5f05cc86b82fc823dbc1dc8bb00f9b5f6f0f621af12f2bbee13815731834b0d85c3653b8ea0ddfc39da235ad78347695d2371ca7654118c276b7bdf3a7bf867492a7867b8dfb287574f52ab91b7b057bc204975232fa1a387a7d96cc8ef660f6f18b5d190df8d1ede18e075607eed3dbc49c06bc7fc423dbc0ce0a1adbc10ece1cd055e2de6d7dac3cb01de09ccef7a0f4fb3cb90df573d3c17f008e6d7dcc3db813c6cb7a61ede3ee4c1ea72e197bdf482bc5ac8ef8b5e7a41de09c8ef2af2e0ba50d70d2367a25e1736aaf8b40eec4c4fbf95def957fd56cb17c72ae9a2731ac8d6dd7e6f1cedb6015abc53b78eb7ff28ee95d5db49646e19e700c659a8f75d37db3d1e46fb704fd68d08ff9be30179c6f1803ce378409e713c20cf381eaa7bca4ec4b213be8af0bf5976f52dcaaebe45d9d5b7281b79dd6347ef87a7d848fbd131400a99081fdb31d286bdf48af235f6e4d75d97d61e5e64cc265cefe145c6e78516da4760cc69656be313f79ef5c8a38dc99e3180bcc8788c8c012ab73e1e2363007991f1181903c88b8cc7c818405e643c46c600f222e33132069017197b913180bcc8d88b8c01e445c69e3606345e64ecedb9d2c3eb1e53977b78dd63beb187d73d461b7a78dd63b4be87770479ed905f5d2f5d45c6a8bf47ff117b7be1d31e5ec4deee39ef031eb72f9344f683d36f8e928be85e70bab7bc794f29ae37707f37ee9f54f5ef17a10c8f2a6d6595c60a5364cf659c34067cba958cd21860712de7f504a09d92a09d3d5d36e847b88f3935f33ca1d7f1e06b847289b255e2b56f77f6bc6bfc3eb27b7ce2f3dbd12116c7750f4fc2efa7ba7a8559ba76d2fd0bdce31c27a1df580ef36640b33f49d4fefcacb7fde96b7b44560dae25711e1853c1820e6f7d3dae29d8be76271207f41a0cf9237307f2f05e9c6707f105c1cf770513bc411fe9651338b50dfce0a004e31dd62e77e178dfa3f7fd624e0d0621cf0cccb39996fb1b1c6f7bdab5bc61dd0c71e23cae48de83b4bc23762e92be8af886d1fbd8a7ba9087f7e33c0283717c424b6f7b82f20c02799aa93ce3a93c2d7a7983a03f69e5093de551b9dea476a009e344f2f6090c8e694e2b37d246d55cd94e17dc230cf2757975bb076ddebb6c8b8f073d5dee295be46f5976f95188b730f0cdb2453e52b6ae0f1ee344ee47caa779bc817944c6879b37ca48e5194de5a9d5ebd157d644bc9770a1979e6e2deb7e2c67cf67b7d0136fd013ff2d7a12fa943d16cb5e58d34b4fc22dcb3e80655ff8e4167a12fa969dc9ab3b058ea63f2772ddf51f45eb7f2612a76ca7c0a83bc09f3c2b70d8af22ba833534e713212df0e97ab01e655f788dda854b15937bc69cfbe1de632e622fd0ce44c65fc4be50db42c843f88cc7c04b83b502a1639ec7f279967eebb341a26b1afc16a18914ce443e962bcf1558ed7ba054585398896d6e90ae5b8cdfaac84221dbd49c10443be8a880311a0ae1de6b51e3efb91ee197d56462ddbaf4f85ff5f0a91dead0e337f7f0aba98fa1c76fd2f812c8948af335ac03cc64cadc208ea59016e7c2b59e38b944b365c85f78b587efc13c3be877e68424f594e5c63ed38afa011d8cd1f8d0b61e68b31a89c3b95dbb77c21ab98772a30cf80c19d286f4fb77e879623febd0e60649c0ef98e28461a4cc3c8ca4566d61408f0770de8fdc8f9beba2f242d9c95afa6ad4e3209cdb9b88fd7b119e2e6717c83ebe8787fa5b08694fdca5f370dc60f83b3d61ccfbc4b84818f26122e56b7ad19e75281bf13b0589daeb15e404955506592365e137ce204f22f603ec473de96bfe8df412caf115ee28fd66fac0bf993ea119d28ffc66fad0bf997ee11790feb648fac8bc148907f73f52f13b974bda1a5c1b7b99965bcd7791b4d0d787617f82f1c56ae36b4f18fb1de4ffa748bedddf8ae963963ea735054d0a1158fcd6401183d1f4f98ea49f31a2c9cf51b9f7bbe9390cd0a788dad646e2e66712b920c4e177e732bec3a8a9405f84d601c72996f79834eca38fdde2478f15846efb5892900ef9d82d7df431841fdb61fee3adc6b15656353de701dafa966551bf5bb31d8c2c255339a17c066d847f63e0a6997e9757cdd2357d9584cf58af3a12798683f898269227ca4cf305b9519648fe283ff64b5a7ff011f393ec2df4bbc321928836d36a0999f01b706568107dec34b0ef90570877fe0ef2bf2a7561580d49cdaaf60c947c5b1cfcc6cd6ae979060371edf83c0ee70ef41b22edd5d43ce892af9ac1e70e16b4373ec813bf7f44bb8dcf05fb7ecf5741bfe7a3e766a01f768bb3331cbdcecef0b6e1d9193504f58ecf1eff4fcece88d877ac8362a2dfdf6872d77e3f7cab73347ac78f407b4e8dcfa645924bb83edf11aa3c0b7e2c4b6c4ea73df25c11bf21c46f8cd412fcee9125e07611d555b50e9f3f369da9dc218b105fdc69c77c6d828b349d58f8dfcac828d1934a9f4f05b4756c66c03383eeb3bd086388c1ef39b9c4ea307d0efd667597bfb1a6cb9349e35fd0fbd56af08bc5a6da3d25d0af1a3c550bf10dee06337e3b24b856e15cd31453785753edc2b7c1cce2f761939a62ec139b624edcadbaa2e8f770308f7ca9dc1645df7f807eae68f46f97cda7f039f28c377bc56b82b4dfd36491baf4fef0357ee78edfc163fdb1eee631da776d9e31f48b950bf47b78fc561ebf89d7bf95d7bebb14616e9863968ba3e8b7ce5abaaacf9b48cc70cf9df4fd72931c15433ce3e07ad3ec0b72740ccb1d73d23d898ea3b076380a724951a44c807ee38a26715177627e43201dab3a79c67fdbac4ef39d78165149b4cac710f3383c8f68f6e76a540ceb48837c26435bb979f27128edcc63a1c967ca5cad61b007f7cb423429738e2649c92d04ecc4fd5c5a14c34d8e228fb9a54b7254142bf33cd344cef4f3ff3205e6e1ad8fc1f534d93c897892e8fbadcfe8b34daf44fb8ca3c285f697e502e0ebe0da655e90555a1b58cee76254d356fca69875c0b54d0ad9ad87eae9b324ff6d4207527c2e886b724cc335084cdc4691d8d606596f4d352b8b84533e3331dc69ec732b0579dd4ca2f026bce6d0661c2f3e455e1f5483df24defc7f950c159a0cc7ff07321c97425486290b4086fd023305ca57451339fe3f9441ebab590c3745a4fdd091348370e38a617dd90fe71f7e4af415fb1467c0ce659e2291f31be8f90e78b603f6e194554ca48f63ff75dc53cc28bc44e2f8d6b0a39aa7dfef2a9b246873ba17885176064c717c5d5879dcce4ce1a74f73f87852d60fe37950172647d5fb442916e8d80a144691ee3cb6802e2279bc123081bde0fac4110a0535944994ed127d0711b9a74cb74b783f2e9ad7e6fd5f4aa26df333e0e31646c95221f1ef0eb4a1ace81b429e6c245dd9e6afc3fe992b615ddb1ac6332594c779fa5e650a1fc273688234ed2b819b2807ce75bde5898bbe19a667061878fe99f67f911ffaa5babc1b9f61d44db7114715c4df2974a17eb0ac385ed7516180ffb7cec788bc8feb5527aca77f6621b67b073eeb06b9b7ebf6b258159bc374fec36f95774b5daa5b24bf4b0af5c7f020d23cc8ff8ad445bf373f2a50df43e6b612d93487a8a672220f2c2730f776aa5da141f4fbefd51f736552f0067d2fb4b89d2d9376dc54ceb713eb215c7f36d079648af4258b733957d5169e023e00ca0469bea6736f5e3b5cefe8a0be3ec3b459d38a4cd6946d266b323d774cb2a64d93ac293f90ace983819725e1b3137c6f6b4d390936116c6814c3aa516758399a700efcce157c056bca45d029c8b653c06fa8a3407f51a03f3cabcb04fa93407f12e84f04fd895066b4f61e600eab6c0cd2ef5f03853c5d6338328b38eb7886b5b20c0fbe41d8dabf857e1b6ab5b468df30a7035f4c667b978bfe8d32441039488b3bbbf07d119d9ffb5f6465a99c5546d68bb23b498f6b86b1124de4cd9b88757c0bab7c06eb3548877be4302ce7ba09a6c31dca4a745054a3fbe19954619817fb29fd27310a9bc1f9e3cfb6a39cb807027d5bed1ad3b5e07387362d8cfe534298b61be85c96a03d3747137f76e8a62c9e64547ac6c6c28b8ea4930c964bcb1b19fc46bda8fc43ea4d7de44fea47d4e8cd44f9442211dd70490cafcc94345942015d6fe5922c82cf161f14315fb518f3fd90e68bf65015b78a0a7b365a69dc3f50150b05853f3b44b9b4dfe427421db68d2acc61fd6cb001fac9c5def5501685d89e3a249ca57500195057b8feb25a189ebe13154f883806a9be2643f9eee104bfd3e6aa445abe3fa6aa5d7527eabce144ffd6b70dda5754a2cf0e5745bbe88f39fb77552c27d6d11ff2ca3c493b47ca7291a7e721411b82bdebcf1d8926ca027c3e7686b509e5762b0bb208451cf8212fcb22e803bfc3d6f61954407b0d81fc254584bad6edc7be053a080e043925ff6dc18f22edaae90fd712764615e7484affb360ff0b257fecd933b02e60501eeabbcf28021be4ba294f1a067efad130be73b7d5ef87f4afb1d837be55f6dd012a27ca0ffa0ad26fa25f09b0ffba3e1736e1f3686e5fcffd297dee276c83fb2d980f8695ed5a5eb856c433a6aca35ba81cf4bb6c682f948196bf8f96dfe1a88aa67a423e8e47ad3d13fee8f0819f2349da984983b2605da444d710ee9444dffbe25a418915bbfb22b4612337b948c0f318ace927b12fe03e9076ed9d66af779ce99aff6a4d790bc6f649f0d53e049feda208fd41b4a631687fa4eba3ed2de6143cabcb3911daefd78ea4e9d0e65234d6017d57ebb85032fab2d6039e64b4a3f4fa4d69bce6df66decd1dc1f7bd453c9ea1a3ed34acda8f6300fac621cc1b74b69f4b7282deeb0662de71ee50d83319f36818a73c544beb44cb191b1a47f3de2b8dc37222ef3a296fbf34b6cf3b4fedb9af897e4f8f79bf5a479f91d07c468512f13d60b7ac07a4d1788e04961d991b34b9f6eca675dd2ed1774dd6c4542ba5a33da330ad5677c9e2df2e811ddfd34ccbd9ddab9cf1a19138f650f7da1e850b1dbdf413af9ace08bdcfd3cc271877cf573defb6f573d5d28a4d9a8f290da6e77215b40fa26723544b743f0f9e312543a65a9cea91747f50ca36c2a58a12ce198e636093261783fd29961c939c02375914b949bc80fb81028526dce706f9bcc1e31a16d7aeb86e877e0bd4de7d3e814dd861dcdf7313cf83a2673fc118d4d618ae3be8b95069fa199b7856541af8fd78b6543acc5be0f7cb3cae83f7d440df64a8ef4fd30909d0afce69beff0747a9ef7f1bfafe9b12fe2ddfbf79cf47dff4fd677d447dffdbd0f7df34f27fc5f76f4ef82bfafedf76a655e40cab9e7323047ac6e4f531e4903911db79c6557d9d7415f2abc6e70cf87ecc5a1e30617b37357fbf6405d1d2cb7856a1083a2bd675acad93cab5757842235d83c0fafc5fc775edd2e22eacd7e25e68fb967cf5b87b2e6971135aff755c41d5e25eb8a0c55dd8e2b820d033b9d4f07d601bc23c9e4151f67935893b5f40ca3e0a90b8cf4324ee02acdf4353f1f98ca40e04fbb8ae9d29db00ebe99f753171dc2072fc975f12e5a510876757a983e8d914ecb7c8bb4d97b74697f7329e231bb7b0d2ae8883889c7ffd3ef981eb61d53c9578a5367aee25ead7c65da73ea94d7a1dcfcd2271b193e8f98d363c37270072427bf9efaf09cbc121442e4d439f86575e1944e70b65c3972c3df7c58914e4dbf92529f3b899b21af7349b49c467ca6c640739f53bdc127d3ee9df30a8cbb65164acc9ed44766574bf1bfb46bde858c8f8b9debe7fd6dbf71f8e1af447b5b1a9fc127ce30302491d8967e80eec8aab9909e5ad23b69f7d897a853a5433719f9f26f2ba102317fc0ce2c490b88fdbc9949170bfbd5d827c58d00d437dd8971a5975e04aad2e9bce73ff5a26e105bdbd4fe8edfd11f58fcffa40d7787e51e83efc624c8db511af6938c31d3111dc6b290f03fd9924d6e6da4fb8c99a4f5d363c15d6a1e788b5b046df676726fee15f7645f46213f6137daefa57fa59aeebe798ae9f0f7a8f35d50d8035e2f5b130deb47e724c36413e2e13d1c79f97fa2438d66af77c84630deac07b2a16a27d5daedcace87e66d43366453d2fe1b7a083cdda7b457b38f2dc88daf3b72bdea767b04ea2cf5f9e88bc33a4cf361b2bf8c8b98c68736dce2a28f330f85be8170a3d672f0a62b77dc53340d1d7a7e7bdd4269c067b49f0cc177aaea65078933edf8a164cda1c2bbd837b86b00c5f3521bdf77adaf0d9e36837ac7baab5333887f2acea9158f9719ec533ba6c756ec87fd0155fe8260b7dc2747c7e904dcd1884751d8d72615c3c37cf3bbf95a5f3404d05eb0d55b0d64375dad991875bfbce0bfa9c05eb5b6dece2de23b150c4398eb6dba13dbfd4f45778b3d73b135a1ffa2c453ba324b3f7b3149a8f50aea5affddb2fc05ece71a4ba05baef373dc4d335ef56890715cce17c6e4ece0dc558dfc133d6f019617518cf3cc36bff48a90bee33b4fc0258d7bc13e839630cf2c3f36c0eba8a663033988c1564d18069838b32b4f356f0a4174dd3b9af6f54d2976dfbd3060f9ee962b9e3f955daac9bfbeb51cde6d1e823bd7fd0e674db23df580d70164d9f5a72f109e5b6ed04bfa18a7ccb027ee908af3b8573f17ff6387ceef006cf8d4aedbb042d97ecfd1ea0aab36e5fd1163cd3e44527334b31ef62b5bbce37b4bbfc1b455b9a48ed507af7f65da4f79925f18b84a83bf80d9ecbbec8d58d4aad0e499e52659c036a9c50125025c9023a1c599ab97ee6fab9caf4465e89e6583936954c2db5a69d8e711c90260833a60c6f20dec6d3e029992758d3ce5b4a6628e68ff1ccd809735fb21eac4ec6d3efa2a481d2f6eab73d5bfcf8add953ab747d6d7da4d5f10789682721e0a908783ac28feb0e6e1abf4978903b5c92c09ce6a777a6c8bb52c8a85dca3f7972eb731894a86868a3a83b145726d3999235e2d3caf8e98ec9514c67aab2d3c3f2ef07cf7149fc8429c3cb88e33b4eb2ea652629aa69c3ced8e90e5f4982fff6821b7eb1b56356a5352d8a7c5a093edd04c7a4a8c12d2f17251505e6b936ecbc58391efe7f50d9f30c583ba5a628839901fd21b32853356de5d03f1be744af7f0a5f6887d6b79093eb1f8f036f3db243bd77fc3302c6579e1488b6d35dacd3529d8827279972a61cbf3810bf78d339eaf226574f2a22184b3931024b49746dc22f0f8762693ddf138c58751729ee686afef9d79df31531482666c4cd3d6b174a1a56e289287a1df49c6bbf29ff50cc7994ab67477eeff8cddf8c3fb86f7c3c2348eb87ce9fee5e84bd14cfdd79b189c988e841352d1aa0e7f2633c57900cc09c2097b97dce084aa2f6f3b98d8b30ddec1d1109bad3764b40a25082dd8bd6cf9dbacb9f30fa6b7c2365bdd7bd7aaa2ba3b2aff48437a6b50b7da5df9d658d0992dd39d6c1f07fd693273f9b65c52fbc1eb60e0b92cf1eb60e87eb555633fc7f3efef23b59e997773f955eff4e4efc17ef2c8b0fbeb336be53931bfceda7372a1b0a57fe3a1c3e06b80678f237e1f0fb00cb6fc3e1f6df85c3e4105c033e83ebdfbc150e1f003a2d6f69eef29c6c4bceead52b56df114d7e343de3feb9994f59962eb7ccc8cbca5f62797845760ed420c2a74ce4dd035196aeb1accd5abd06aed6e4ac5ebb72cdade3e5ad58b112ee6c250c8005700984f0f71d22024004480013200a100de807e80f7106000602060162c8566e30d021905e063a14e830c070b836036213b6921180db00718078c0ed80918004800570076014c00a180d180348042401c602ee048c037c077017e0bb80f180ef0192011300130136400a6012603220159006b81b700f600a201d702f602ae03e801d649d0672de0ff401a0d38166009d0175cd04cc04cc02cc063c087808f030e01188f37dc01cc00f008f821e1e03fa38dc9b0b79cc03cc87f002c013801f42f8478027014f017e0cbc858045802cc0d30005900dc881fb8b01b9802580a5c07b06f02c200fb00cb01cb002b012eeaf02ac06e403d6006f2de0394001601db4f0f3809f007e0a7801b01ef0b3985de4e7804240116003c00170421a17a018e086fc36024a009b009b015b002f025e02bc0cc0bf6d805792771115b01dae770076023c8052c02ec8eb5540196037e017805f02f600ca01af01f642bcd7016f00f601f6032a807f00f02bc041c0af01bf01fc16f026e077804310ef2dc0ef015ec061c011e01f05f800c7007f00bc0d380e7807f02ee03d4025e004e024e08f803f01de0754014e01aa01a7011f00fe0cf80be0af80ff02fc37e06f8033800f011f013e067c02380b329c03d400ce033e050dfd3d6117f9075c7f06f827e07368830b406b21fe45c025801f5007a8073400028046c065c015c055c035c017802f014d8066400be02bc075402b200868038400ed801b809b80af011d804e4017200c20d875b5df631a193d470f3f4e431f85c3e980c7014b002f00b602de001c03fc15500b6805983e0e87e30176c002401ea010b01d7000f00ee00ca00ed00ee8ff09d83d400a6026e049c04a8013500a5804d808f80de00ca011d00188391b0e8f06a4011e026403d601b600ca01fc394853130e9f00c4c2f561e0dd05d40e5800c8031402b6030e00de019c01cc84f87540db01fde1da027812b012900270024a01a721bf5a40fa799011b00e500a380cf804d00a88f9341c1e0b980ec8036c04bc0138016804dcf58f70780e600980ff3b840173007980ed804380bf021a01317adc458035801700a701b580b19f810e00d98076fdde0b701df399768df787fe1328201de0046c07de21c019c01c082f02acfca71637828774fa3ef0f9cfa11e803a08f3ffecb9ff905e468422d2219e05301a7017200580fd68edf22559cbb3f36046a393923e2b81cd5bb13ac792bd76d9ca7b2c63f2a30979206b6d7e8e2579dd98e40993f3d6dd65c998f34077086e93d539b93f1a933c31fba9b163f2efb4dcdb2be6bfba134dcb59fcaf6e8f1db3f8cebb2cdf7a1b7398339314925bd08eefdd9a1fa129ff37f763277dfbfdb1cb349aac53bb4e67ea74814e17e974a54ed7e974a34eb7eab45ca707747ac2103e6d089fd1e9799db6eb942cd7688c4e63756a378467ea748e4e571ac2eb0ce1424378a321bcd5102ed569b94e0f19c2c70ce11386f06943f88c217cde10ae35841b0de16643b87d795f7d92157dc3264338c6102e7d56cf4fa7870ce16386f00943f8b4217cc6103e6f08d71ac28d3a6dd6a929af6f38460fc7ea34cd10b6eb74a64e1719c24b0ce19586f03a43b8d010dea8d3ad3a3d60081f32848f19c2270ce1d386f01943f8bc215c6b08371ac2cd3a6dd769ccb2bee15843d862082f5e936c413a638e654dceb2952b5667ad5e9a938ffc0916edfe449dda749aa2d3493a9dacd3548de677e7979ff51c9863307c4bf3c122e76b93fee27c3ddfacee7859ab73d72ecb59be26ff7bab73d6ac5dbddcf25c56deda9c48fcac48fc89df88df3be2e22c5dbe2c5dbe2c5dbe2c5dbeac887cddf93c766bf9f47cf2f57cf2f57cf2f57cf223f9a4e9f46e9d4e48d6e9044d1f6991721eefd1abe581acbcbc9cd5f4be9e6e8d9e6e8d96ee2739ab57e08d9959abb3bf5bb0743548882cb23acb6241fea39a8ab2b2b357e7e483ccf92b35fe636bb294672d2b57d0d90fc2b93a3f336fc5d359793d37d6e8fcc797accec9caeec54fd6f9b712167ed01de87de80e94e6277f6f31aeeefa2af17b8b57672dcbe92506c9d7d3f58d076b12bdbc196b972b6b96ae58feedbd00e2ebf9644dfc57e9fa2680eea0c54fd1e9249d4ed669aa5e8f89b7922f9fe4ebe9f3f5f4f97afa7c3d7d7e247d9a4eefd6e984649d4ed0f4a5e7b346cf678d9ecf1a3d9f4373b571f8994e4df3f4795da78b74ba75be46dfd1e9a1057af8b16fa74f43333f4bdb23329ed6e6adb12cce59a32ce915ce5b9195ddfb7efe1a709b68782974815ce8404b97e7af59bd96aa9cf2972dcdcfd21f21f464d6974f33ed15eec9f43f7ec77ffc8efff81dffbff63be27e943cc53661598665ec98bcb577c2daec1ecbf7d7aeb1ac586c5996b36cc5eae7a321c23258183df6c4630f4c7be8212dbe6dd9fc5ef1f39fcf5f98b36ee91a8b82f354b6e5e9e72d74b537262fdb52b074cd125c63c1f24ccbe816e5cd5d9ebf76254c766bd0363d9f8fb960da7b2c59c9f78ec1155fd6049d4e447a4744a4ff673f5123bc812df40eb02cf95ffb49ff93c8dcbf1df3337dfef98d3ecfacd4e7aff82734ba46a72b7fa8d1c37abc4f747ee437f3a77abfd0e98117fadefffffa8fd1e940db7ffd3a65c8b2866b9f0ba41ff9cfef3fbffffcfef3fbcfef3fbfffdddf82f5fafa49a74b0c749d816e34d052033d60a0c70cf4b4819e37d046036d3750d3cffad258031d6ba069063ad3401718e812035d67a01b0db4d4400f18e831033d6da0e70db4d140db0dd4f4f3be34d640c71a689a81ce34d00506bac440d719e846032d35d003067acc404f1be879036d34d076033515f6a5b1063ad640d30c74a6812e30d02506bace40371a68a9811e30d063067ada40cf1b68a381b61ba8a9a82f8d35d0b1069a66a0330d7481812e31d07506bad1404b0df480811e33d0d3067ade401b0db4dd404d1bfad258031d6ba069063ad3401718e812035d67a01b0db4d4400f18e831033d6da0e70db4d140db0dd4e4e84b630d74ac81a619e84c035d60a04b0c749d816e34d052033d60a0c70cf4b4819e37d046036d375093b32f8d35d0b1069a66a0330d7481812e31d07506bad1404b0df480811e33d0d3067ade401b0db4dd404daebe34d640c71a689a81ce34d0c82f1cd6de8bc73ca73f9fd16958ff11d2fd8c41bb60bac369f4ffcb7c9ffcfe2f1b4c67a9'

ISP_PROG = binascii.unhexlify(ISP_PROG)

#print('ISP_FLASH progam size (compressed)', len(ISP_PROG))
ISP_PROG = zlib.decompress(ISP_PROG)
#print('ISP_FLASH progam size (decompressed)', len(ISP_PROG))

def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█'):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + '-' * (length - filledLength)
print('\r%s |%s| %s%% %s' % (prefix, bar, percent, suffix), end = '\r')
# Print New Line on Complete
if iteration == total:
print()

def slip_reader(port):
partial_packet = None
in_escape = False

while True:
waiting = port.inWaiting()
read_bytes = port.read(1 if waiting == 0 else waiting)
if read_bytes == b'':
raise Exception("Timed out waiting for packet %s" % ("header" if partial_packet is None else "content"))
for b in read_bytes:

if type(b) is int:
b = bytes([b]) # python 2/3 compat

if partial_packet is None: # waiting for packet header
if b == b'\xc0':
partial_packet = b""
else:
raise Exception('Invalid head of packet (%r)' % b)
elif in_escape: # part-way through escape sequence
in_escape = False
if b == b'\xdc':
partial_packet += b'\xc0'
elif b == b'\xdd':
partial_packet += b'\xdb'
else:
raise Exception('Invalid SLIP escape (%r%r)' % (b'\xdb', b))
elif b == b'\xdb': # start of escape sequence
in_escape = True
elif b == b'\xc0': # end of packet
yield partial_packet
partial_packet = None
else: # normal byte in packet
partial_packet += b


class ISPResponse:
class ISPOperation(Enum):
ISP_ECHO = 0xC1
ISP_NOP = 0xC2
ISP_MEMORY_WRITE = 0xC3
ISP_MEMORY_READ = 0xC4
ISP_MEMORY_BOOT = 0xC5
ISP_DEBUG_INFO = 0xD1

class ErrorCode(Enum):
ISP_RET_DEFAULT = 0
ISP_RET_OK = 0xE0
ISP_RET_BAD_DATA_LEN = 0xE1
ISP_RET_BAD_DATA_CHECKSUM = 0xE2
ISP_RET_INVALID_COMMAND = 0xE3

@staticmethod
def parse(data):
op = data[0]
reason = data[1]
text = ''
try:
if ISPResponse.ISPOperation(op) == ISPResponse.ISPOperation.ISP_DEBUG_INFO:
text = data[2:].decode()
except ValueError:
print('Warning: recv unknown op', op)

return (op, reason, text)


class FlashModeResponse:
class Operation(Enum):
ISP_DEBUG_INFO = 0xD1
ISP_NOP = 0xD2
ISP_FLASH_ERASE = 0xD3
ISP_FLASH_WRITE = 0xD4
ISP_REBOOT = 0xD5
ISP_UARTHS_BAUDRATE_SET = 0xD6
FLASHMODE_FLASH_INIT = 0xD7

class ErrorCode(Enum):
ISP_RET_DEFAULT = 0
ISP_RET_OK = 0xE0
ISP_RET_BAD_DATA_LEN = 0xE1
ISP_RET_BAD_DATA_CHECKSUM = 0xE2
ISP_RET_INVALID_COMMAND = 0xE3

@staticmethod
def parse(data):
op = data[0]
reason = data[1]
text = ''
if FlashModeResponse.Operation(op) == FlashModeResponse.Operation.ISP_DEBUG_INFO:
text = data[2:].decode()

return (op, reason, text)


def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]


class MAIXLoader:
def change_baudrate(self, baudrate):
self._port = serial.Serial(
port=port,
baudrate=baudrate,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=0.1
)

def __init__(self, port='/dev/ttyUSB1', baudrate=115200):
# configure the serial connections (the parameters differs on the device you are connecting to)
self._port = serial.Serial(
port=port,
baudrate=baudrate,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=0.1
)
print(INFO_MSG,"Selected Baudrate: ",baudrate, BASH_TIPS['DEFAULT'])

self._port.isOpen()
self._slip_reader = slip_reader(self._port)

""" Read a SLIP packet from the serial port """

def read(self):
return next(self._slip_reader)

""" Write bytes to the serial port while performing SLIP escaping """

def write(self, packet):
buf = b'\xc0' \
+ (packet.replace(b'\xdb', b'\xdb\xdd').replace(b'\xc0', b'\xdb\xdc')) \
+ b'\xc0'
#print('[WRITE]', binascii.hexlify(buf))
return self._port.write(buf)

def read_loop(self):
out = b''
# while self._port.inWaiting() > 0:
# out += self._port.read(1)

# print(out)
while 1:
sys.stdout.write('[RECV] raw data: ')
sys.stdout.write(binascii.hexlify(self._port.read(1)).decode())
sys.stdout.flush()

def recv_one_return(self):
timeout_init = time.time()
data = b''
# find start boarder
#sys.stdout.write('[RECV one return] raw data: ')
while 1:
if time.time() - timeout_init > timeout:
raise TimeoutError
c = self._port.read(1)
#sys.stdout.write(binascii.hexlify(c).decode())
sys.stdout.flush()
if c == b'\xc0':
break

in_escape = False
while 1:
if time.time() - timeout_init > timeout:
raise TimeoutError
c = self._port.read(1)
#sys.stdout.write(binascii.hexlify(c).decode())
sys.stdout.flush()
if c == b'\xc0':
break

elif in_escape: # part-way through escape sequence
in_escape = False
if c == b'\xdc':
data += b'\xc0'
elif c == b'\xdd':
data += b'\xdb'
else:
raise Exception('Invalid SLIP escape (%r%r)' % (b'\xdb', b))
elif c == b'\xdb': # start of escape sequence
in_escape = True

data += c

#sys.stdout.write('\n')
return data

def reset_to_isp_kd233(self):
self._port.dtr = False
self._port.rts = False
time.sleep(0.01)
#print('-- RESET to LOW, IO16 to HIGH --')
# Pull reset down and keep 10ms
self._port.dtr = True
self._port.rts = False
time.sleep(0.01)
#print('-- IO16 to LOW, RESET to HIGH --')
# Pull IO16 to low and release reset
self._port.rts = True
self._port.dtr = False
time.sleep(0.01)

def reset_to_isp_dan(self):
self._port.dtr = False
self._port.rts = False
time.sleep(0.01)
#print('-- RESET to LOW, IO16 to HIGH --')
# Pull reset down and keep 10ms
self._port.dtr = False
self._port.rts = True
time.sleep(0.01)
#print('-- IO16 to LOW, RESET to HIGH --')
# Pull IO16 to low and release reset
self._port.rts = False
self._port.dtr = True
time.sleep(0.01)

def reset_to_boot(self):
self._port.dtr = False
self._port.rts = False
time.sleep(0.01)
#print('-- RESET to LOW --')
# Pull reset down and keep 10ms
self._port.dtr = True
self._port.rts = False
time.sleep(0.01)
#print('-- RESET to HIGH, BOOT --')
# Pull IO16 to low and release reset
self._port.rts = False
self._port.dtr = False
time.sleep(0.01)

def greeting(self):
self._port.write(b'\xc0\xc2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0')
op, reason, text = ISPResponse.parse(self.recv_one_return())

#print('MAIX return op:', ISPResponse.ISPOperation(op).name, 'reason:', ISPResponse.ErrorCode(reason).name)


def flash_greeting(self):
while 1:
self._port.write(b'\xc0\xd2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0')
op, reason, text = FlashModeResponse.parse(self.recv_one_return())
#print('MAIX return op:', FlashModeResponse.Operation(op).name, 'reason:',
# FlashModeResponse.ErrorCode(reason).name)
if FlashModeResponse.Operation(op) == FlashModeResponse.Operation.ISP_NOP:
print(INFO_MSG,"Boot to Flashmode Successfully",BASH_TIPS['DEFAULT'])
break

print('wait 1 sec')
time.sleep(1)

def boot(self, address=0x80000000):
print(INFO_MSG,"Booting From " + hex(address),BASH_TIPS['DEFAULT'])
out = struct.pack('II', address, 0)

crc32_checksum = struct.pack('I', binascii.crc32(out) & 0xFFFFFFFF)

out = struct.pack('HH', 0xc5, 0x00) + crc32_checksum + out # op: ISP_MEMORY_WRITE: 0xc3
self.write(out)

def recv_debug(self):
op, reason, text = ISPResponse.parse(self.recv_one_return())
#print('[RECV] op:', ISPResponse.ISPOperation(op).name, 'reason:', ISPResponse.ErrorCode(reason).name)
if text:
print('-' * 30)
print(text)
print('-' * 30)
if ISPResponse.ErrorCode(reason) not in (ISPResponse.ErrorCode.ISP_RET_DEFAULT, ISPResponse.ErrorCode.ISP_RET_OK):
print('Failed, retry, errcode=', hex(reason))
return False
return True

def flash_recv_debug(self):
op, reason, text = FlashModeResponse.parse(self.recv_one_return())
#print('[Flash-RECV] op:', FlashModeResponse.Operation(op).name, 'reason:',
# FlashModeResponse.ErrorCode(reason).name)
if text:
print('-' * 30)
print(text)
print('-' * 30)

if FlashModeResponse.ErrorCode(reason) not in (FlashModeResponse.ErrorCode.ISP_RET_OK, FlashModeResponse.ErrorCode.ISP_RET_OK):
print('Failed, retry')
return False
return True

def init_flash(self, chip_type):
chip_type = int(chip_type)
print(INFO_MSG,"Selected Flash: ",("In-Chip", "On-Board")[chip_type],BASH_TIPS['DEFAULT'])
out = struct.pack('II', chip_type, 0)
crc32_checksum = struct.pack('I', binascii.crc32(out) & 0xFFFFFFFF)

out = struct.pack('HH', 0xd7, 0x00) + crc32_checksum + out

sent = self.write(out)
op, reason, text = FlashModeResponse.parse(self.recv_one_return())
#print('MAIX return op:', FlashModeResponse.Operation(op).name, 'reason:',
# FlashModeResponse.ErrorCode(reason).name)

def flash_dataframe(self, data, address=0x80000000):
DATAFRAME_SIZE = 1024
data_chunks = chunks(data, DATAFRAME_SIZE)
#print('[DEBUG] flash dataframe | data length:', len(data))
total_chunk = math.ceil(len(data)/DATAFRAME_SIZE)

for n, chunk in enumerate(data_chunks):
while 1:
#print('[INFO] sending chunk', i, '@address', hex(address), 'chunklen', len(chunk))
out = struct.pack('II', address, len(chunk))

crc32_checksum = struct.pack('I', binascii.crc32(out + chunk) & 0xFFFFFFFF)

out = struct.pack('HH', 0xc3, 0x00) + crc32_checksum + out + chunk # op: ISP_MEMORY_WRITE: 0xc3
sent = self.write(out)
#print('[INFO]', 'sent', sent, 'bytes', 'checksum', binascii.hexlify(crc32_checksum).decode())

address += len(chunk)

if self.recv_debug():
break
printProgressBar(n+1, total_chunk, prefix = 'Downloading ISP:', suffix = 'Complete', length = 50)

def dump_to_flash(self, data, address=0):
'''
typedef struct __attribute__((packed)) {
uint8_t op;
int32_t checksum; // 下面的所有字段都要参与checksum的计算
uint32_t address;
uint32_t data_len;
uint8_t data_buf[1024];
} isp_request_t;
'''

DATAFRAME_SIZE = 4096
data_chunks = chunks(data, DATAFRAME_SIZE)
#print('[DEBUG] flash dataframe | data length:', len(data))



for n, chunk in enumerate(data_chunks):
#print('[INFO] sending chunk', i, '@address', hex(address))
out = struct.pack('II', address, len(chunk))

crc32_checksum = struct.pack('I', binascii.crc32(out + chunk) & 0xFFFFFFFF)

out = struct.pack('HH', 0xd4, 0x00) + crc32_checksum + out + chunk
#print("[$$$$]", binascii.hexlify(out[:32]).decode())
sent = self.write(out)
#print('[INFO]', 'sent', sent, 'bytes', 'checksum', crc32_checksum)
self.flash_recv_debug()
address += len(chunk)



def flash_erase(self):
#print('[DEBUG] erasing spi flash.')
self._port.write(b'\xc0\xd3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0')
op, reason, text = FlashModeResponse.parse(self.recv_one_return())
#print('MAIX return op:', FlashModeResponse.Operation(op).name, 'reason:',
# FlashModeResponse.ErrorCode(reason).name)

def install_flash_bootloader(self, data):
# 1. 刷入 flash bootloader
self.flash_dataframe(data, address=0x80000000)

def flash_firmware(self, firmware_bin: bytes, aes_key: bytes = None):
#print('[DEBUG] flash_firmware DEBUG: aeskey=', aes_key)

# 固件加上头
# 格式: SHA256(after)(32bytes) + AES_CIPHER_FLAG (1byte) + firmware_size(4bytes) + firmware_data

aes_cipher_flag = b'\x01' if aes_key else b'\x00'

# 加密
if aes_key:
firmware_bin = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=b'\x00' * 16).encrypt(firmware_bin)

firmware_len = len(firmware_bin)

total_chunk = math.ceil(firmware_len/4096)

data = aes_cipher_flag + struct.pack('I', firmware_len) + firmware_bin

sha256_hash = hashlib.sha256(data).digest()

firmware_with_header = data + sha256_hash

# 3. 分片刷入固件
data_chunks = chunks(firmware_with_header, 4096) # 4kb for a sector

for n, chunk in enumerate(data_chunks):
chunk = chunk.ljust(4096, b'\x00') # align by 4kb

# 3.1 刷入一个dataframe
#print('[INFO]', 'Write firmware data piece')
self.dump_to_flash(chunk, address=n * 4096)
printProgressBar(n+1, total_chunk, prefix = 'Downloading Program:', suffix = 'Complete', length = 50)


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--port", help="COM Port", default="DEFAULT")
parser.add_argument("-c", "--chip", help="SPI Flash type, 1 for in-chip, 0 for on-board", default=1)
parser.add_argument("-b", "--baudrate", type=int, help="UART baudrate for uploading firmware", default=115200)
parser.add_argument("-l", "--bootloader", help="bootloader bin path", required=False, default=None)
parser.add_argument("-k", "--key", help="AES key in hex, if you need encrypt your firmware.", required=False, default=None)
parser.add_argument("-v", "--verbose", help="increase output verbosity", default=False,
action="store_true")
parser.add_argument("firmware", help="firmware bin path")

args = parser.parse_args()
if args.port == "DEFAULT":
try:
list_port_info = next(serial.tools.list_ports.grep(VID_LIST_FOR_AUTO_LOOKUP)) #Take the first one within the list
print(INFO_MSG,"COM Port Auto Detected, Selected ",list_port_info.device,BASH_TIPS['DEFAULT'])
_port = list_port_info.device
except StopIteration:
print(ERROR_MSG,"No vaild COM Port found in Auto Detect, Check Your Connection or Specify One by"+BASH_TIPS['GREEN']+'`--port/-p`',BASH_TIPS['DEFAULT'])
sys.exit(1)
else:
_port = args.port
print(INFO_MSG,"COM Port Selected Manually: ",_port,BASH_TIPS['DEFAULT'])

loader = MAIXLoader(port=_port, baudrate=args.baudrate)

# 1. Greeting.
print(INFO_MSG,"Trying to Enter the ISP Mode...",BASH_TIPS['DEFAULT'])

retryCount = 0

while 1:
retryCount = retryCount + 1
if retryCount > 15:
print("\n" + ERROR_MSG,"No vaild Kendryte K210 found in Auto Detect, Check Your Connection or Specify One by"+BASH_TIPS['GREEN']+'`-p '+('/dev/ttyUSB0', 'COM3')[sys.platform == 'win32']+'`',BASH_TIPS['DEFAULT'])
sys.exit(1)
try:
print('.', end='')
loader.reset_to_isp_dan()
loader.greeting()
break
except TimeoutError:
pass

try:
print('_', end='')
loader.reset_to_isp_kd233()
loader.greeting()
break
except TimeoutError:
pass
timeout = 3
print()
print(INFO_MSG,"Greeting Message Detected, Start Downloading ISP",BASH_TIPS['DEFAULT'])
# 2. flash bootloader and firmware
firmware_bin = open(args.firmware, 'rb')

# install bootloader at 0x80000000
if args.bootloader:
loader.install_flash_bootloader(open(args.bootloader, 'rb').read())
else:
loader.install_flash_bootloader(ISP_PROG)

loader.boot()

print(INFO_MSG,"Wait For 1sec for ISP to Boot",BASH_TIPS['DEFAULT'])

time.sleep(2)

loader.flash_greeting()

loader.init_flash(args.chip)

if args.key:
aes_key = binascii.a2b_hex(args.key)
if len(aes_key) != 16:
raise ValueError('AES key must by 16 bytes')

loader.flash_firmware(firmware_bin.read(), aes_key=aes_key)
else:
loader.flash_firmware(firmware_bin.read())

# 3. boot
loader.reset_to_boot()
print(INFO_MSG,"Rebooting...",BASH_TIPS['DEFAULT'])

try:
while 1:
out = b''
while loader._port.inWaiting() > 0:
out += loader._port.read(1)
print("".join(map(chr, out)), end='')
except KeyboardInterrupt:
sys.exit(0)