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编码转换过程
将二进制数据每3字节分成1组,不足3字节的在末尾添0补足3字节
将每组24bit(3*8=24)数据,以6bit为1组分成4组
若6bit数据组中的数据为0则将数据加上0x60
否则将6bit数据组中的数据加上0x20
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 |
起始码
:每一行数据为一帧,并由字符:
作为起始码字节长度
:指示数据内容字段中的字节数。其占1Byte长度,所以数据内容的最大长度为255(0xFF)地址
:表示了数据的起始储存器地址偏移量。其占2Byte长度,所以地址的最大偏移量为216=64Kb。 哪超过64Kb的内容又该如何表示呢?指定类型
:定义了该行数据的具体含义。其占1Byte长度,具体含义表如下:00
: 表示后面的数据内容是用来记录数据的01
: 用来标识文件结束,放在文件的最后,标识HEX文件的结尾。数据字段为空并且地址字段通常为002
: 用来标识扩展段地址的记录,数据字段包含一个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)
数据内容
:n字节数据序列,由2n个16进制数字的ASCII表示校验码
:校验码 = 0x100 - (校验码之前所有16进制数据的累加和)
示例分析
Hex文件中的某行内容::020000040800F2
,该行内容表示扩展线性地址记录,
在它后面的记录数据的基地址为:0x0800<<16 = 0x08000000,它正好是STM32的Flash起始地址。
该帧数据的校验码:0x100 - (0x02+0x00+0x00+0x04+0x08+0x00) = 0xF2
地址计算示例
:020000040108EA
线性基地址:0x0108 << 16 = 0x01080000:0200000212FFBD
扩展地址:0x12FF << 4 = 0x12FF0:0401000090FFAA5502
数据偏移地址:0x0100
上面表示将数据0x90FFAA55
写入到地址:0x01080000 + 0x12FF0 + 0x0100 = 0x010930F0处
:04000005080000ED02 表示程序的入口地址为0x080000ED
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命令都以单个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
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)