Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

一、概述

1. 为什么用网线烧录芯片

传统烧录方式多依赖 JTAG、SWD、UART 或 USB 下载器,需要物理靠近设备、逐台连接。当芯片或产品板已具备以太网接口(或通过串口转以太网模块联网)时,可以通过网线完成:

  • 固件传输:把 .bin / .hex 文件从 PC 发到设备
  • 在线烧录:设备 Bootloader 接收数据并写入 Flash
  • 批量升级:产线、现场设备远程维护

典型场景:工业网关、STM32+W5500 方案、带网口的 PLC、物联网网关批量升级。

2. 整体架构

1
2
3
4
5
6
7
8
9
┌─────────────┐      网线(TCP/IP)      ┌──────────────────┐
│ PC 上位机 │ ──────────────────► │ 目标芯片/设备 │
│ Csharp 工具 │ ◄────────────────── │ Bootloader │
└─────────────┘ 应答/进度/校验 └────────┬─────────┘
│ 写 Flash

┌──────────────┐
│ 程序存储区 │
└──────────────┘

PC 端负责读固件、分包、校验、重传;设备端 Bootloader 负责擦除、写入、校验、重启。

二、常见实现方式

1. 自定义 TCP 协议(本文重点)

在应用层定义简单命令帧,通过 TCP 长连接传输固件块。优点是协议可控、Csharp 实现直观,适合自有 Bootloader。

2. TFTP / FTP

部分 Bootloader(如 U-Boot)支持 TFTP 拉取固件。PC 端可搭建 TFTP 服务器,设备主动下载。Csharp 可调用现有 TFTP 库或配合第三方工具。

3. HTTP OTA

设备作为 HTTP Client 从服务器下载固件,或通过 HTTP POST 上传。适合已有 Web 能力的模块(如 ESP32、Linux 嵌入式)。

4. 与 JTAG 远程调试的区别

JTAG over IP 多用于调试,烧录仍常走专用工具链。本文侧重产品化固件升级:业务协议 + Bootloader + 上位机工具。

三、协议设计示例

1. 设计原则

  • 固定帧头,便于 Bootloader 识别
  • 每条命令带序号,支持 ACK/NACK
  • 数据块带 CRC32,防止传输错误写入 Flash
  • 分阶段:握手 → 擦除 → 传输 → 校验 → 复位

2. 命令定义

命令 说明
HELLO 0x01 握手,交换版本与 Flash 容量
ERASE 0x02 擦除目标区域
DATA 0x03 传输固件数据块
VERIFY 0x04 整包 CRC 校验
RESET 0x05 复位并跳转 App
ACK 0x80 成功应答
NACK 0x81 失败应答

3. DATA 帧结构(示例)

1
2
3
4
5
+--------+--------+----------+----------+---------+----------+
| Magic | Cmd | Seq | Offset | Length | Payload | CRC32 |
| 2 byte | 1 byte | 2 byte | 4 byte | 2 byte | N byte | 4 byte|
+--------+--------+----------+----------+---------+----------+
Magic: 0x55AA

块大小常用 512 / 1024 / 4096 字节,需与 MCU Flash 页大小对齐。

四、Csharp 上位机实现

1. 环境准备

  • .NET 6 或 .NET 8 SDK
  • 创建控制台项目:
1
2
dotnet new console -n FirmwareNetFlasher
cd FirmwareNetFlasher

2. 固件读取与 CRC 计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.IO;
using System.IO.Hashing;

public static class FirmwareFileHelper
{
public static byte[] ReadFirmware(string path)
{
if (!File.Exists(path))
throw new FileNotFoundException("固件文件不存在", path);

return File.ReadAllBytes(path);
}

public static uint CalcCrc32(byte[] data)
{
return BitConverter.ToUInt32(Crc32.Hash(data), 0);
}
}

3. 协议帧封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using System.Buffers.Binary;

public enum BootCommand : byte
{
Hello = 0x01,
Erase = 0x02,
Data = 0x03,
Verify = 0x04,
Reset = 0x05,
Ack = 0x80,
Nack = 0x81
}

public static class BootFrameBuilder
{
private const ushort Magic = 0x55AA;

public static byte[] BuildDataFrame(ushort seq, uint offset, ReadOnlySpan<byte> payload)
{
int frameLen = 2 + 1 + 2 + 4 + 2 + payload.Length + 4;
byte[] frame = new byte[frameLen];
int index = 0;

BinaryPrimitives.WriteUInt16BigEndian(frame.AsSpan(index, 2), Magic);
index += 2;
frame[index++] = (byte)BootCommand.Data;
BinaryPrimitives.WriteUInt16BigEndian(frame.AsSpan(index, 2), seq);
index += 2;
BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(index, 4), offset);
index += 4;
BinaryPrimitives.WriteUInt16BigEndian(frame.AsSpan(index, 2), (ushort)payload.Length);
index += 2;
payload.CopyTo(frame.AsSpan(index, payload.Length));
index += payload.Length;

uint crc = BitConverter.ToUInt32(System.IO.Hashing.Crc32.Hash(frame.AsSpan(0, index)), 0);
BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(index, 4), crc);
return frame;
}
}

4. TCP 连接与发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

public class NetFlasherClient : IAsyncDisposable
{
private readonly TcpClient _client = new();
private NetworkStream? _stream;

public async Task ConnectAsync(string ip, int port, CancellationToken token = default)
{
await _client.ConnectAsync(ip, port, token);
_stream = _client.GetStream();
_client.ReceiveTimeout = 5000;
_client.SendTimeout = 5000;
}

public async Task SendFrameAsync(byte[] frame, CancellationToken token = default)
{
if (_stream == null)
throw new InvalidOperationException("尚未连接设备");

await _stream.WriteAsync(frame, token);
await _stream.FlushAsync(token);
}

public async Task<byte[]> ReceiveFrameAsync(int maxLength = 256, CancellationToken token = default)
{
if (_stream == null)
throw new InvalidOperationException("尚未连接设备");

byte[] buffer = new byte[maxLength];
int read = await _stream.ReadAsync(buffer, token);
if (read <= 0)
throw new IOException("设备连接已断开");

byte[] result = new byte[read];
Array.Copy(buffer, result, read);
return result;
}

public async ValueTask DisposeAsync()
{
_stream?.Dispose();
_client.Close();
await Task.CompletedTask;
}
}

5. 完整烧录流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using System.Threading;
using System.Threading.Tasks;

public class FirmwareFlasher
{
private const int ChunkSize = 1024;
private readonly NetFlasherClient _client = new();

public async Task FlashAsync(string ip, int port, string firmwarePath, IProgress<int>? progress = null)
{
byte[] firmware = FirmwareFileHelper.ReadFirmware(firmwarePath);
uint totalCrc = FirmwareFileHelper.CalcCrc32(firmware);

await _client.ConnectAsync(ip, port);

// 1. 握手
await SendHelloAsync();

// 2. 擦除
await SendEraseAsync((uint)firmware.Length);

// 3. 分包写入
ushort seq = 0;
for (int offset = 0; offset < firmware.Length; offset += ChunkSize)
{
int len = Math.Min(ChunkSize, firmware.Length - offset);
byte[] chunk = new byte[len];
Array.Copy(firmware, offset, chunk, 0, len);

byte[] frame = BootFrameBuilder.BuildDataFrame(seq, (uint)offset, chunk);
await SendWithRetryAsync(frame);

seq++;
progress?.Report((offset + len) * 100 / firmware.Length);
}

// 4. 校验
await SendVerifyAsync(totalCrc);

// 5. 复位运行
await SendResetAsync();

await _client.DisposeAsync();
Console.WriteLine("烧录完成");
}

private async Task SendWithRetryAsync(byte[] frame, int maxRetry = 3)
{
for (int i = 0; i < maxRetry; i++)
{
await _client.SendFrameAsync(frame);
byte[] resp = await _client.ReceiveFrameAsync();
if (resp.Length > 0 && resp[0] == (byte)BootCommand.Ack)
return;
}
throw new Exception("设备应答失败,烧录中断");
}

private Task SendHelloAsync() => Task.CompletedTask;
private Task SendEraseAsync(uint size) => Task.CompletedTask;
private Task SendVerifyAsync(uint crc) => Task.CompletedTask;
private Task SendResetAsync() => Task.CompletedTask;
}

上述 SendHelloAsync 等方法需按实际协议补全帧内容;核心流程为:连接 → 擦除 → 分包发送 → 校验 → 复位

6. 程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Program
{
static async Task Main(string[] args)
{
string ip = args.Length > 0 ? args[0] : "192.168.1.100";
int port = args.Length > 1 ? int.Parse(args[1]) : 9000;
string firmware = args.Length > 2 ? args[2] : "app.bin";

var progress = new Progress<int>(p => Console.Write($"\r进度: {p}%"));
var flasher = new FirmwareFlasher();

try
{
await flasher.FlashAsync(ip, port, firmware, progress);
}
catch (Exception ex)
{
Console.WriteLine($"烧录失败: {ex.Message}");
}
}
}

运行示例:

1
dotnet run -- 192.168.1.100 9000 app.bin

五、设备端 Bootloader 要点

设备端通常用 C 语言编写(与芯片 SDK 配套),PC 端用 Csharp。Bootloader 需实现:

  1. 上电判断:按键/GPIO/标志位进入升级模式
  2. TCP 监听:绑定固定端口,等待 PC 连接
  3. Flash 操作:按页擦除、按地址写入
  4. 应答机制:每收到 DATA 帧校验 CRC 后回 ACK/NACK
  5. 跳转 App:烧录完成后关闭中断,设置栈指针,跳转到应用程序入口

Bootloader 占用空间要小,常放在 Flash 起始扇区;App 从偏移地址运行,链接脚本需与 PC 端固件地址一致。

六、硬件与网络配置

1. 典型硬件组合

方案 说明
MCU + 以太网 PHY 如 STM32 + LAN8720,Bootloader 内实现 TCP/IP 协议栈(lwIP)
MCU + W5500 硬件 TCP/IP,减轻 MCU 负担
串口转以太网模块 模块网口接 PC,串口接目标 MCU,PC 仍用 TCP 连接模块
带网口的 Linux 板 可用 Csharp 工具 + SSH/脚本配合,或直接 socket 写 MCU

2. 网络设置

  • PC 与设备处于同一网段,如 PC 192.168.1.10,设备 192.168.1.100
  • 先用 ping 确认连通
  • 确认防火墙未拦截目标端口(如 9000)
  • 产线环境建议固定 IP,避免 DHCP 变化导致烧录失败

七、调试与排错

1. Wireshark 抓包

过滤条件:

1
tcp.port == 9000 && ip.addr == 192.168.1.100

可观察:TCP 三次握手是否成功、每包 DATA 是否有 ACK、是否存在重传。

2. 常见问题

现象 可能原因 处理
连接超时 IP/端口错误、设备未进 Bootloader Ping + 确认监听端口
写入后无法启动 链接地址错误、向量表偏移不对 核对 Flash 布局与 hex/bin 起始地址
CRC 校验失败 大小端不一致、块长度不对齐 统一 BigEndian/LittleEndian
中途断开 包过大、看门狗复位 缩小 ChunkSize、喂狗

3. 日志建议

Csharp 上位机建议记录:时间戳、序号、偏移、重试次数、每阶段耗时。产线问题可追溯具体哪一台、哪一包出错。

八、安全与量产建议

  • 版本校验:握手阶段比对硬件版本号,防止错刷固件
  • 数字签名:Bootloader 验证固件签名后再写入
  • 断电保护:双区备份(A/B 分区),写入完成并校验通过再切换启动区
  • 权限控制:限制仅内网网段可连接烧录端口
  • 批量脚本:Csharp 工具配合 CSV 设备列表,循环 IP 批量升级

九、小结

通过网线烧录芯片的本质,是 TCP/IP 传输 + Bootloader 写 Flash + PC 上位机工具 三者配合。Csharp 适合编写 Windows 工控机上的烧录软件:文件读取、Socket 通讯、进度界面、日志记录都很方便。

学习与实践路径:

  1. 先用 Netcat 或简易 TCP 工具验证设备端口可达
  2. 定义并文档化 Bootloader 协议
  3. 用 Csharp 实现分包发送与 CRC 校验
  4. 配合 Wireshark 联调
  5. 再在 WinForms / WPF 中封装为可视化烧录工具

与《网络通讯协议与抓包工具学习》结合,可以从协议分层到实际上位机开发形成完整链路。

评论