2. LPC1752串口ISP#

2.1. UU编码#

最近在学习LPC1752的ISP功能时,单片机在ISP时接收上位机发送的数据是经过UU编码的。初次接触到UU编码(UUencode)

1. UU编码是啥

UU编码是一种将二进制数据转换为ASCII码的编码方式,UU编码是以3字节二进制数据为组进行ASCII转换的,如果要转换的数据不足3字节则先在末尾补0使其长度为3字节后再进行转换。 每行UU编码的二进数据不能超过45(45/3=15)字节,编码后的字符长度不超过61(15*4=60)。

2. 每行UU编码的格式

<character length><formatted characters><newline>

  • <character length>: 它是一个字符,它指示了该行UU编码的进行制数据长度。它是二进制数据长度再加上32后对应的ASCII字符。

  • <formatted characters>: 它是二进制数据,经过UU编码后的ASCII编码字符串

  • <newline>: <CR><LF>回车换行符

3. UU编码转换过程

  1. 将二进制数据每3字节分成1组,不足3字节的在末尾添0补足3字节

  2. 将每组24bit(3*8=24)数据,以6bit为1组分成4组

  3. 若6bit数据组中的数据为0则将数据加上0x60

  4. 否则将6bit数据组中的数据加上0x20

UU编码

import binascii

data = b'\x14\x0f\xa8'
# 若不指定backtick=True,侧0x00将表示为<空格>而不是字符`
encode = binascii.b2a_uu(data,backtick=True)

print("{0} UUEncode: {1}".format(data,encode))

# 上面输出结果为:b'\x14\x0f\xa8' UUEncode: b'#%`^H\n'
# UU编码的第1个字符'#'表示数据长度:我们输入的二进制数据长度为3,所以长字符就是 `3+0x20=0x23=#`

print(binascii.a2b_uu(encode))
b'\x14\x0f\xa8' UUEncode: b'#%`^H\n'
b'\x14\x0f\xa8'
/**
 *  @brief 将3字节二进制数据转换为4字节的UU编码字符
 *  @param [in] chasc: 3字节的原始数据
 *  @param [out] chuue: 4字节UU编码字符数据
 */
void	Uue (unsigned char chasc[3],unsigned char chuue[4])
{
	int i,k=2;
	unsigned char t=NULL;
	for(i=0;i<3;i++) 
	{
		*(chuue+i)=*(chasc+i)>>k;
		*(chuue+i)|=t;
		if(*(chuue+i)==NULL)	*(chuue+i)+=0x60;
		else					*(chuue+i)+=0x20;
		t=*(chasc+i)<<(8-k);
		t>>=2;
		k+=2;
	}
	*(chuue+3)=*(chasc+2)&63;
	if(*(chuue+3)==NULL) *(chuue+3)+=0x60;
	else				*(chuue+3)+=0x20;
}

2.2. Hex文件格式#

Hex是intel规定的标准,hex的全称是IntelHEX。此类文件通常用于传输将被存于ROM或EEPROM中的程序和数据。 是由一行行符合IntelHEX文件格式的文本构成的ASCII文本文件。

整个文件以行为单位,每行以冒号开头,内容全部为16进制码,2个ASCII码字符表示1字节16进制数据。 Hex文件中的每行格式定义:

起始码

字节长度

地址

指令类型

数据内容

校验码

:

1Byte

2Byte

1Byte

0-255Byte

1Byte

  1. 起始码:每一行数据为一帧,并由字符:作为起始码

  2. 字节长度:指示数据内容字段中的字节数。其占1Byte长度,所以数据内容的最大长度为255(0xFF)

  3. 地址:表示了数据的起始储存器地址偏移量。其占2Byte长度,所以地址的最大偏移量为216=64Kb。 哪超过64Kb的内容又该如何表示呢?

  4. 指定类型:定义了该行数据的具体含义。其占1Byte长度,具体含义表如下:

    • 00: 表示后面的数据内容是用来记录数据的

    • 01: 用来标识文件结束,放在文件的最后,标识HEX文件的结尾。数据字段为空并且地址字段通常为0

    • 02: 用来标识扩展段地址的记录,数据字段包含一个16bit的段基址(因此字节数始终为02),地址段被忽略, 最近的段地址02记录乘以16(右移4位),然后加到每个后续数据记录地址,以形成数据的物理起始地址。 这允许寻址多达1Mb(16bit右移4位形成共20位地址,220=1Mb)的地址空间

    • 03: 开始段地址记录,对于x86处理器,指定CS:IP寄存器的初始内容(即起始执行地址), 地址段为0,字节长度始终为4,前两字节为CS值后两字节为IP值

    • 04: 用来标识线性基地址记录, 地址段将被忽略,字节长度为2字节,两个数据字段为所有后续类型指定32位地址的高16位

    • 05: 开始线性地址记录(程序入口地址),地址字段为0未使用,字节长为4,4个数据字节代表一个32位地址(big-endian)

  5. 数据内容:n字节数据序列,由2n个16进制数字的ASCII表示

  6. 校验码:校验码 = 0x100 - (校验码之前所有16进制数据的累加和)

示例分析

Hex文件中的某行内容::020000040800F2,该行内容表示扩展线性地址记录, 在它后面的记录数据的基地址为:0x0800<<16 = 0x08000000,它正好是STM32的Flash起始地址。 该帧数据的校验码:0x100 - (0x02+0x00+0x00+0x04+0x08+0x00) = 0xF2

地址计算示例

  1. :020000040108EA 线性基地址:0x0108 << 16 = 0x01080000

  2. :0200000212FFBD 扩展地址:0x12FF << 4 = 0x12FF0

  3. :0401000090FFAA5502 数据偏移地址:0x0100

上面表示将数据0x90FFAA55写入到地址:0x01080000 + 0x12FF0 + 0x0100 = 0x010930F0处

:04000005080000ED02 表示程序的入口地址为0x080000ED

程序入口

Python的Hex处理库intelhex

import intelhex

# 打印hex文件内容
with open(r'./lpc_usart_isp/test.hex','r') as f:
    print(f'1. ./lpc_usart_isp/test.hex 文件内容:')
    print(f.read())

# 将hex文件转换为bin文件
print(f'2. 将 ./lpc_usart_isp/test.hex 转换为 ./lpc_usart_isp/test.bin')
intelhex.hex2bin(r'./lpc_usart_isp/test.hex',r'./lpc_usart_isp/test.bin')

with open(r'./lpc_usart_isp/test.bin','rb') as f:
    print(f'3. ./lpc_usart_isp/test.bin 文件内容:')
    print(intelhex.hexlify(f.read()).decode())

# 将bin文件转换为hex文件
# intelhex.bin2hex(r'./lpc_usart_isp/test.bin',r'test.hex')
# intelhex.hexlify(b'\x08\x00')

import subprocess

# print(subprocess.run("dir",shell=True,stdout=subprocess.PIPE).stdout.decode('gb2312'))

# p = subprocess.Popen('dir .',shell=True,stdout=subprocess.PIPE)
# p.wait()

# p = subprocess.Popen('ls -l', stdout=subprocess.PIPE)
# output, error = p.communicate()
# print(output.decode())

# p = subprocess.Popen(['hex2dump',r'./test.hex'],stdout=subprocess.PIPE,shell=True)
# output,error = p.communicate()
# print(output.decode())

# 调用intelhex库中的hex2dump命令行脚解析并显示hex文件
print("4. ./lpc_usart_isp/test.hex 文件解析后的内容")
p = subprocess.run(['hex2dump',r'./lpc_usart_isp/test.hex'],stdout=subprocess.PIPE,shell=True)
print(p.stdout.decode())

# 调用intelhex库中的hex2bin命令行脚本将hex文件转换成bin格式的文本输出
print("5. ./lpc_usart_isp/test.hex 文件转换成bin后的内容")
p = subprocess.run(['hex2bin',r'./lpc_usart_isp/test.hex'],shell=True,stdout=subprocess.PIPE)
print(p.stdout.hex())

print('hex文件信息:')
p = subprocess.run(['hexinfo',r'./lpc_usart_isp/test.hex'],shell=True,stdout=subprocess.PIPE)
print(p.stdout.decode())
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[2], line 1
----> 1 import intelhex
      3 # 打印hex文件内容
      4 with open(r'./lpc_usart_isp/test.hex','r') as f:

ModuleNotFoundError: No module named 'intelhex'

2.3. LPC1752 串口ISP#

对Flash编程有2种方式:

  • 在系统编程(ISP): 是通过Boot装载软件和UART0串口对片内Flash进行编程或再编程的方法

  • 在应用编程(IAP): 是用户代码对片内Flash进行编程或再编程的方法

Flash Boot 代码在芯片上电或复位后最先执行。Boot 代码可以执行 ISP 程序或用户的应用代码。 发生硬件复位后,P2.10 引脚为低电平,这就被当作启动 ISP 命令处理器的外部硬件请求。 假定在/RESET 引脚上出现上升沿时,电源引脚出现正确的信号,那么在采样 P2.10 之前有 3ms 的时间决定是执行用户代码还是 ISP 处理程序。如果 P2.10 为低电平且看门狗溢出标志置位, 那么忽略启动ISP命令处理器的外部硬件请求。在没有ISP命令处理器的请求(硬件复位后P2.10 引脚为高电平)时,将搜索有效的用户程序。若发现有效的用户程序,执行控制权就被转移给 用户程序。若没有找到有效的用户程序,就将调用自动波特率程序。

引脚 P2.10 的状态作为 ISP 硬件请求时需要特别注意:由于 P2.10 在复位后处于高阻模式, 所以要使该引脚的状态稳定,用户需要提供外部硬件(上拉电阻或其它器件)。否则可能就进 入了 ISP 模式。

2.3.1. 用户代码是否有效#

有效用户代码的判定标准:保留的 Cortex-M3 向量单元(除向量单元 7 以外,位于向量表 0x001C)应当包含表入口 0~6 的校验和的 2 的补码,这样就使前 8 个表入口的校验和为 0。Boot 代码首先计算 Flash 扇区 0 中前 8 个中断向量的校验和。如果结果为 0,执行控制权便转移给用 户代码。

2.3.2. 代码读保护(CRP)#

代码读保护机制允许用户使能系统中的不同安全级别以便访问片内 Flash 和限制 ISP 的使用。 需要时,可通过在 Flash 地址单元 0x000002FC 编程特定的格式来调用 CRP。 IAP 命令不受代码读保护的影响

2.3.3. 通信协议#

ISP数据序列

  • 所有ISP命令都以单个ASCII字符串形式发送

  • 字符串应以回车<CR>或换行<LF>控制字符作为结束符,多余的<CR>和<LF>将忽略

  • 数据是以UU编码格式发送和接收

2.3.4. IPS命令#

2.3.4.1. 解锁#

  • 命令: U

  • 输入参数: 23130

  • 返回代码:

    • CMD_SUCCESS

    • INVALID_CODE

    • PARAM_ERROR

该命令用于解锁Flash写、擦除和运行命令.

示例:U 23130 <CR><LF>

2.3.4.2. 设置波特率#

  • 命令: B

  • 输入参数:

    • 波特率 9600|19200|38400|57600|115200|230400

    • 停止位 1|2

  • 返回代码:

    • CMD_SUCCESS

    • INVALID_BAUD_RATE

    • INVALID_STOP_BIT

    • PARAM_ERROR

该命令用于改变波特率,新的波特率在命令处理程序发送CMD_SUCCESS返回代码之后生效

示例: B 115200 1 <CR><LF>

2.3.4.3. 回应#

  • 命令: A

  • 输入参数: 设定值 1(打开),0(关闭)

  • 返回代码:

    • CMD_SUCCESS

    • PARAM_ERROR

该命令用于设定在收到ISP命令后是否做回应

示例: A 0 <CR><LF>

2.3.4.4. 写RAM#

  • 命令: W

  • 输入参数:

    • 起始地址 被写RAM的起始地址。该地址应当以字为边界

    • 字节数 写入的字节数。 该数值应当为4的倍数

  • 返回代码:

    • CMD_SUCCESS

    • ADDR_ERROR

    • ADDR_NOT_MAPPED

    • COUNT_ERROR

    • PARAM_ERROR

    • CODE_READ_PROTECTION_ENABLED

该命令用于将数据下载到RAM。数据应当为UU编码格式。当代码读保护使能时该命令禁止

示例: W 1073742336 4 <CR><LF> 向地址0x40000200写入4个字节数据

2.3.4.5. 读存储器#

  • 命令: R

  • 输入参数:

    • 起始地址 被读出数据字节的起始地址。该地址应当以字为边界

    • 字节数 读出的字节数。 该数值应当为4的倍数

  • 返回代码:

    • CMD_SUCCESS

    • ADDR_ERROR

    • ADDR_NOT_MAPPED

    • COUNT_ERROR

    • PARAM_ERROR

    • CODE_READ_PROTECTION_ENABLED

该命令用于读出RAM或Flash存储器的数据。数据为UU编码格式。当代码读保护使能时该命令禁止

示例: R 1073741824 4 <CR><LF> 向地址0x40000000读出4个字节数据

2.3.4.6. 准备写操作的扇区#

  • 命令: P

  • 输入参数:

    • 起始扇区号

    • 结束扇区号

  • 返回代码:

    • CMD_SUCCESS

    • BUSY

    • INVALID_SECTOR

    • PARAM_ERROR

该命令必须在执行“将 RAM 内容复制到 Flash”或“擦除扇区”命令之前执行。 这两个命令的成功执行会导致相关的扇区再次被保护。 该命令不能用于 Boot Block。 要准备单个扇区,可将起始和结束扇区号设置为相同值

示例: P 0 0<CR><LF>准备 Flash 扇区 0

2.3.4.7. 将RAM内容复制到Flash#

  • 命令: C

  • 输入参数:

    • Flash地址: 要写入数据字节的目标 Flash 地址。目标地址的边界应当为 256 字节

    • RAM地址: 读出数据字节的源 RAM 地址

    • 字节数: 写入的字节数目。应当为 256 | 512 | 1024 | 4096

  • 返回代码:

    • CMD_SUCCESS

    • SRC_ADDR_ERROR

    • DST_ADDR_ERROR

    • SRC_ADDR_NOT_MAPPED

    • DST_ADDR_NOT_MAPPED

    • COUNT_ERROR

    • SECTOR_NOT_PREPARED_FOR_WRITE_OPERATION

    • BUSY

    • CMD_LOCKED

    • PARAM_ERROR

    • CODE_READ_PROTECTION_ENABLED

该命令用于编程 Flash 存储器。“准备写操作的扇区”命令应当在该命令之前被执行。当成功执行复 制命令后,受影响的扇区将自动再次受到保护。当代码读保护使能时该命令被禁止

示例: C 0 1073774592 512<CR><LF>将 RAM 地址 0x4000 8000 开始的 512 字节复制到 Flash 地址 0

开源LPC串口ISP程序

import binascii

data = binascii.a2b_uu('$______')
print(data)
print(hex(int.from_bytes(data,byteorder='little')))

checksum = 0
for e,i in enumerate(data):
    checksum += i

print(checksum)
import serial
import serial.tools.list_ports

print([i.name for i in serial.tools.list_ports.comports()])


with serial.Serial(port='COM5',baudrate=115200,timeout=0.001) as ser:
    if(not ser.is_open):
        ser.open()
    
    while True:
        ser.write(b'?\r\n')
        r_dat = ser.readall()
        print(r_dat)
        if r_dat.find(b'Synchronized') >= 0:
            ser.write(b'Synchronized\n')
            # if(ser.read_all().endswith(b'Synchronized')):
            #     ser.write(b'\r\n')
            break
        if r_dat.endswith(b'1\r\n'):
            ser.write(b'\x1b\n')
    
    while True:
        r_dat = ser.readline()
        print(r_dat)
        if r_dat.find(b'OK\r\n'):
            ser.write(f'{0:d}\n'.format(16000).encode())
            break

    while True:
        r_dat = ser.readline()
        print(r_dat)
        if r_dat.endswith(b'OK\r\n'):
            ser.write(b'J\n')
            break

    while True:
        r_dat = ser.readline()
        print(r_dat)
        if r_dat.endswith(b'OK\r\n'):
            ser.write(b'G 0 T\n')
            break

    
    
import intelhex
import binascii
import struct
import sys
import filecmp


# 将Hex文件转换成bin文件
intelhex.hex2bin(r'c:\Users\TGL233\Desktop\TH-ISE130-I\Project\CMSIS\Cortex_M3\Objects\CM3_SP.hex',r'CM3_SP.bin')

# 计算新的用户代码有效校验值,并写入bin文件对应位置
with open(r'CM3_SP.bin','rb+') as f:
    bin_dat = f.read(32)
    print(binascii.hexlify(bin_dat,' ',4))

    # 重新计算用户代码有效效验值,并写入bin文件中
    unpack_dat = struct.unpack('8I',bin_dat)
    check_value = (0-sum(unpack_dat)) & 0xFFFFFFFF
    print('check_value: {0:#08x} -> {1}'.format(check_value,binascii.hexlify(struct.pack('<I',check_value),' ',1)))
    f.seek(28)
    f.write(struct.pack('<I',check_value))
    f.flush()

    # 对比新旧前32字节内容差异
    f.seek(0)
    new_bin_dat = f.read(32)
    print('    bin_dat: {0}'.format(binascii.hexlify(bin_dat,' ',1).decode()))
    print('new_bin_dat: {0}'.format(binascii.hexlify(new_bin_dat,' ',1).decode()))


# 将bin文件中每45字节数据转换成UU编码,并写入文件中

with open(r'CM3_SP.bin','rb') as f_b :
    # bin_size = f_b.__sizeof__()
    # bin_size = sys.getsizeof(f_b)
    bin_size = len(f_b.read())
    print("文件{0}共有{1}字节数据".format(f_b.name,bin_size))
    count = 0
    f_b.seek(0)
    count_str = ''
    with open(r'CM3_SP.uu','w+') as f_u :
        while count < bin_size:
            if count + 45 <= bin_size:
                bin_dat = f_b.read(45)
                count += 45
            else:
                bin_dat = f_b.read(bin_size-count)
                count += bin_size -count
            uu_dat = binascii.b2a_uu(bin_dat,backtick=True)
            count_str  += '{0:d} '.format(count)
            # print(uu_dat)
            f_u.write(uu_dat.decode())
        print(count_str)
            
import serial

with serial.Serial(port='COM5',baudrate=115200,timeout=0.001) as ser:
    if not ser.is_open:
        ser.open()
    else:
        ser.write(b'?\n')
        if(ser.readall().endswith(b'Synchronized\r\n')):
            ser.write(b'Synchronized\n')
            print('Synchronized')
        else:
            raise Exception('Synchronized')

        if(ser.readall().endswith(b'OK\r\n')):
            ser.write(b'16000\n')
            print('Initialized!')
        else:
            raise Exception('Inition')
        
        if(ser.readall().endswith(b'OK\r\n')):
            print('{0:s} ISP mode {0:s}'.format('='*20))
        else:
            raise Exception('In ISP')
        
        
import serial

COM_PORT = 'COM5'
BAUDRATE = 115200
TIMEOUT = 0.001

ser = serial.Serial()
state = 'uninitialized'

try:
    ser = serial.Serial(COM_PORT,BAUDRATE,timeout=TIMEOUT)
    if not ser.is_open:
        ser.open()
    print(ser)
except serial.SerialException as err:
    print(err.args[0])
else:
    ser.write(b'?\n')
    if ser.readall().endswith(b'Synchronized\r\n'):
        ser.write(b'Synchronized\n')
        
        
finally:
    ser.close()
import serial
import serial.tools.list_ports
import binascii
import intelhex
import tempfile

class lpc1752_isp(object):
    '''
    LPC1752的ISP编程工具类
    '''

    def __init__(self,port:str = None,baudrate:int = 115200,timeout:float = 0.001,*args,**kagrs) -> object:
        super.__init__()

    @staticmethod
    def hex2bin(f_source:str,f_dest) -> bytearray:
        pass

    def init(self):
        try:
            if serial in kagrs:
                self._ser = serial
            else:
                self._ser = serial.Serial(port,baudrate,timeout=timeout)
            if not self._ser.is_open:
                self.open()
        except serial.SerialException as err:
            print("The system serial port list:")
            for port, desc, hwid in sorted(serial.tools.list_ports.comports()):
                print("\t{}: {} [{}]".format(port, desc, hwid))
            print(err)
        
        self._bin_file = None
        self._state = None


                
# try:
#     isp = lpc1752_isp('COM15')
# except serial.SerialException as err:
#     for port, desc, hwid in sorted(serial.tools.list_ports.comports()):
#         print("{}: {} [{}]".format(port, desc, hwid))
#     print(err)
#     # raise
                
isp = lpc1752_isp('COM15')
The system serial port list:
	COM5: JLink CDC UART Port (COM5) [USB VID:PID=1366:0105 SER=000023520130 LOCATION=1-1:x.0]
could not open port 'COM15': FileNotFoundError(2, '系统找不到指定的文件。', None, 2)