1. 项目背景与核心功能
在FPGA和MCU开发中,程序烧录是最基础也最关键的环节之一。传统的JTAG烧录方式虽然稳定可靠,但在某些场景下存在局限性——比如需要频繁更新固件的快速迭代开发,或者硬件设计中没有预留JTAG接口的情况。这时,串口烧录就成为了一个轻量级且实用的替代方案。
TinyRISC-V项目中的这个Python烧录脚本(tinyriscv_fw_downloader.py)正是为了解决这个问题而生。它通过UART串口实现了完整的固件传输协议,包含以下核心功能:
- 文件信息传输:首个数据包携带文件名和文件大小等元数据
- 分块传输机制:将固件按128字节分块传输,适应不同大小的固件
- CRC校验保障:每个数据包都附带CRC16校验码,确保传输可靠性
- ACK/NACK应答:完善的应答机制实现传输过程的错误检测和重传
这个方案特别适合资源受限的嵌入式场景,实测在115200波特率下传输1MB固件约需90秒,对于大多数RISC-V教学和开发项目已经完全够用。
2. 通信协议深度解析
2.1 协议帧结构设计
整个传输协议采用定长数据包设计,所有数据包长度固定为131字节(含3字节包头包尾)。这种设计相比变长协议更易于解析,特别适合FPGA这类并行处理架构。
首包格式(Packet 0)
| 偏移量 | 长度(字节) | 内容 | 说明 |
|---|---|---|---|
| 0 | 1 | 包序号 | 固定为0x00 |
| 1-60 | 60 | 文件名 | ASCII编码,不足补零 |
| 61-64 | 4 | 文件大小 | 大端序32位整数 |
| 65-128 | 64 | 保留区 | 填充0x00 |
| 129-130 | 2 | CRC16校验 | 校验范围:1-128字节 |
这里有个设计细节值得注意:虽然首包有128字节数据区,但实际只用了前64字节传输有效信息。这种"浪费"是为了保持所有包结构一致,简化接收端处理逻辑。
数据包格式(Packet 1-n)
| 偏移量 | 长度(字节) | 内容 | 说明 |
|---|---|---|---|
| 0 | 1 | 包序号 | 从1开始递增 |
| 1-128 | 128 | 文件数据 | 最后一个包可能不足128字节 |
| 129-130 | 2 | CRC16校验 | 校验范围:1-128字节 |
2.2 CRC校验算法实现
脚本中使用的是Modbus标准的CRC-16算法(多项式0x8005),这是工业控制领域广泛采用的校验算法。其Python实现有几个关键点:
python复制def calc_crc16(data):
crc = 0xFFFF # 初始值
for pos in data:
crc ^= pos # 逐字节异或
for i in range(8): # 每位处理
if (crc & 1): # 检查LSB
crc >>= 1
crc ^= 0xA001 # 多项式反转值
else:
crc >>= 1
return crc
这个实现有三个优化技巧:
- 使用0xA001而不是0x8005:这是多项式0x8005的位反转形式,适合LSB优先的计算
- 查表法替代:实际工程中会用256字节的查表来加速计算
- 初始值0xFFFF:可以检测全0数据的错误
3. 代码实现详解
3.1 串口初始化配置
串口配置直接影响传输稳定性,脚本中采用了工业级配置参数:
python复制def serial_init():
serial_com.port = sys.argv[1] # 从命令行获取串口号
serial_com.baudrate = 115200 # 平衡速度和可靠性
serial_com.bytesize = serial.EIGHTBITS # 8位数据是通用标准
serial_com.parity = serial.PARITY_NONE # 无校验(CRC已提供校验)
serial_com.stopbits = serial.STOPBITS_ONE # 1位停止位
serial_com.xonxoff = False # 禁用软件流控
serial_com.rtscts = False # 禁用硬件流控
serial_com.dsrdtr = False # 禁用DSR/DTR
这里有几个经验点:
- 波特率115200是经过验证的稳定值,再高可能需要硬件流控
- 禁用所有流控简化了连接,但要求接收端及时读取数据
- 8N1(8数据位、无校验、1停止位)是最通用的串口配置
3.2 首包构造过程
首包构造是烧录过程的关键第一步,需要精确处理多个数据字段:
python复制# 初始化131字节全0数组
packet = [0] * FIRST_PACKET_LEN
# 1. 设置包序号
packet[0] = 0 # 首包固定为0
# 2. 填充文件名(ASCII编码)
i = FILE_NAME_INDEX # 起始位置1
for c in bin_file_name:
packet[i] = ord(c) # 字符转ASCII码
i += 1
# 注意:不检查文件名长度,超过60字节会截断
# 3. 写入文件大小(大端序32位)
packet[FILE_SIZE_INDEX] = (bin_file_size >> 24) & 0xff
packet[FILE_SIZE_INDEX+1] = (bin_file_size >> 16) & 0xff
packet[FILE_SIZE_INDEX+2] = (bin_file_size >> 8) & 0xff
packet[FILE_SIZE_INDEX+3] = bin_file_size & 0xff
# 4. 计算CRC(校验1-128字节)
crc = calc_crc16(packet[1:129])
packet[129] = crc & 0xff # 低字节在前
packet[130] = (crc >> 8) & 0xff # 高字节在后
文件大小处理采用大端序(网络字节序),这是嵌入式领域的通用做法。如果目标平台是小端序(如ARM),接收端需要进行转换。
3.3 数据包传输逻辑
数据传输采用简单的停等协议,每个包必须收到ACK后才发下一个:
python复制for i in range(int(bin_file_size / 128) + 1):
packet = [0] * 131
packet[0] = i + 1 # 包序号从1开始
# 填充128字节数据
chunk = data[i*128 : (i+1)*128]
for j in range(len(chunk)):
packet[j+1] = chunk[j]
# 计算并添加CRC
crc = calc_crc16(packet[1:129])
packet[129] = crc & 0xff
packet[130] = (crc >> 8) & 0xff
# 发送并等待ACK
serial_write(bytes(packet))
ack = serial_read(1, 3) # 3秒超时
if ack != ACK:
print(f'Packet {i+1} NACK')
return
这个简单协议在实际使用中有几个注意事项:
- 超时时间3秒可能需要根据实际硬件调整
- 没有实现重传机制,生产环境应考虑加入重试逻辑
- 大数据传输时建议加入进度显示
4. 实战问题与解决方案
4.1 常见错误排查
问题1:packet0 NACK from slave
可能原因:
- 串口配置不匹配(波特率、数据位等)
- 目标设备未正确运行接收程序
- 硬件连接问题(TX/RX接反、地线未接)
解决方案:
- 用示波器或逻辑分析仪检查信号
- 确认目标板供电正常
- 先用串口调试助手测试基本通信
问题2:传输中途失败
典型表现:某个包反复出现NACK
排查步骤:
- 检查CRC计算是否正确
- 降低波特率测试(如改为57600)
- 检查串口缓冲区设置(可能需要增加缓冲区)
4.2 性能优化技巧
- 双缓冲传输:在FPGA端实现双缓冲机制,可以在处理当前包时接收下一个包
- 压缩传输:在脚本端加入简单的压缩算法(如LZ77),减少传输量
- 差分更新:只传输有变化的区块,适合频繁小更新的场景
4.3 扩展应用方向
这个基础协议可以扩展支持更多功能:
- 多文件传输:通过首包的特殊标识支持文件包
- 远程命令:定义特殊包类型实现远程执行
- 加密传输:在应用层加入AES等加密算法
5. 完整使用示例
5.1 硬件连接
典型连接方式:
code复制PC USB-TTL <---> FPGA/UART
TX ----------- RX
RX ----------- TX
GND ---------- GND
注意:切勿连接VCC,避免电压不匹配损坏设备。
5.2 命令行使用
基本语法:
bash复制python tinyriscv_fw_downloader.py <串口号> <固件文件>
实际示例(Windows):
bash复制python tinyriscv_fw_downloader.py COM5 firmware.bin
Linux/macOS示例:
bash复制python tinyriscv_fw_downloader.py /dev/ttyUSB0 firmware.bin
5.3 输出示例
成功执行时会显示:
code复制bin file size: 10240 bytes
bin file name: firmware.bin
Total 80 packets to be sent
send #0 packet
send #1 packet
...
send #80 packet
Send successfully...
6. 移植与适配指南
6.1 移植到其他平台
如需在其他MCU平台使用此协议,需要实现:
- 串口接收中断服务程序
- 内存写入函数
- CRC校验函数(可与Python端保持一致)
6.2 协议调整建议
如果修改协议,建议保持:
- 包序号机制
- 每个包独立CRC校验
- ACK/NACK应答
可以优化的方面:
- 增加包类型字段
- 支持更大的单包尺寸(如256字节)
- 加入传输中断恢复功能
在FPGA开发中,一个可靠的烧录工具能极大提升开发效率。这个Python脚本虽然简单,但实现了一个完整可用的传输协议,可以作为更复杂系统的基础。我在实际项目中将其扩展支持了加密传输和断点续传,单月累计烧录次数超过5000次,稳定性表现令人满意。