使用 C# 实现 OTA 固件升级流程

OTA 固件升级是指设备在不拆机的情况下,通过网络、蓝牙、串口或其他通信方式更新内部固件。

在实际项目中,OTA 并不是简单地把固件文件发送给设备。一个相对完整的升级流程通常还需要处理设备状态检查、固件校验、分包发送、超时重试、升级进度以及失败恢复。

下面以 C# 上位机程序为例,实现一个简单的 OTA 固件升级流程。

一、梳理 OTA 升级的基本流程

一个常见的 OTA 升级过程可以分为以下几个阶段。

第一步,读取固件文件,并检查文件是否存在。

第二步,计算固件大小和哈希值,确认文件没有损坏。

第三步,通知设备进入升级模式,并发送固件基本信息。

第四步,将固件拆分成多个数据块,按照顺序发送给设备。

第五步,每发送一个数据块,都要等待设备确认。

第六步,全部发送完成后,通知设备校验完整固件。

第七步,设备确认升级成功后重启,并运行新版本固件。

整个过程中,设备和 C# 程序之间需要有一套明确的升级协议。例如,可以约定以下几类指令:

进入升级模式。

发送固件信息。

发送固件数据块。

结束固件传输。

查询升级结果。

重启设备。

实际的指令编号、数据格式和校验方式,需要根据具体设备协议确定。

二、设计固件分包格式

固件文件通常比较大,不能一次性发送给设备,因此需要分成多个数据块。

假设每个数据块最大为 1024 字节,数据包中可以包含以下内容:

指令类型。

数据块编号。

数据块长度。

数据内容。

CRC 或其他校验值。

数据块编号非常重要。设备可以根据编号判断是否存在重复数据、数据丢失或者顺序错误。

例如,一个 5000 字节的固件文件,按照每块 1024 字节拆分,需要发送 5 个数据块。

前 4 个数据块都是 1024 字节,最后一个数据块是 904 字节。

除了数据块校验,还应该对完整固件计算哈希值:

byte[] firmware =

await File.ReadAllBytesAsync(

firmwarePath,

cancellationToken);

byte[] firmwareHash =

SHA256.HashData(firmware);

升级开始前,C# 程序将固件大小和哈希值发送给设备。

全部数据发送完成后,设备再次计算固件哈希值,并与升级开始时收到的值进行比较。只有两者一致,才能说明固件传输完整。

三、定义设备升级通信接口

不同设备使用的通信方式可能不同。为了让升级流程不直接依赖 TCP、串口或蓝牙,可以定义一个统一接口:

public interface IOtaTransport

{

Task EnterUpgradeModeAsync(

int firmwareSize,

byte[] firmwareHash,

CancellationToken cancellationToken);

Task SendBlockAsync(

    int blockIndex,

    byte[] blockData,

    CancellationToken cancellationToken);

Task CompleteUpgradeAsync(

    byte[] firmwareHash,

    CancellationToken cancellationToken);

Task RebootDeviceAsync(

    CancellationToken cancellationToken);

}

EnterUpgradeModeAsync 用于通知设备准备接收固件。

SendBlockAsync 用于发送一个固件数据块,并等待设备确认。

CompleteUpgradeAsync 用于通知设备固件传输完成,同时执行完整性校验。

RebootDeviceAsync 用于通知设备重启。

具体项目中,可以分别实现 TcpOtaTransport、SerialOtaTransport 或 BluetoothOtaTransport。

这些类负责数据包组装、发送、接收和响应解析,而 OTA 服务只负责控制整个升级流程。

四、实现 OTA 固件升级服务

下面实现一个简单的升级服务:

public sealed class OtaUpgradeService

{

private const int BlockSize = 1024;

private const int MaxRetries = 3;

private readonly IOtaTransport _transport;

public event Action<int>? ProgressChanged;

public OtaUpgradeService(IOtaTransport transport)

{

    _transport = transport;

}

public async Task UpgradeAsync(

    string firmwarePath,

    CancellationToken cancellationToken)

{

    if (!File.Exists(firmwarePath))

    {

        throw new FileNotFoundException(

            "没有找到固件文件。",

            firmwarePath);

    }

    byte[] firmware =

        await File.ReadAllBytesAsync(

            firmwarePath,

            cancellationToken);

    if (firmware.Length == 0)

    {

        throw new InvalidDataException(

            "固件文件内容为空。");

    }

    byte[] firmwareHash =

        SHA256.HashData(firmware);

    await _transport.EnterUpgradeModeAsync(

        firmware.Length,

        firmwareHash,

        cancellationToken);

    int blockCount =

        (firmware.Length + BlockSize - 1)

        / BlockSize;

    for (int blockIndex = 0;

        blockIndex < blockCount;

        blockIndex++)

    {

        cancellationToken

            .ThrowIfCancellationRequested();

        int offset = blockIndex * BlockSize;

        int currentSize = Math.Min(

            BlockSize,

            firmware.Length - offset);

        byte[] blockData =

            new byte[currentSize];

        Buffer.BlockCopy(

            firmware,

            offset,

            blockData,

            0,

            currentSize);

        await SendBlockWithRetryAsync(

            blockIndex,

            blockData,

            cancellationToken);

        int progress =

            (blockIndex + 1) * 100

            / blockCount;

        ProgressChanged?.Invoke(progress);

    }

    await _transport.CompleteUpgradeAsync(

        firmwareHash,

        cancellationToken);

    await _transport.RebootDeviceAsync(

        cancellationToken);

}

private async Task SendBlockWithRetryAsync(

    int blockIndex,

    byte[] blockData,

    CancellationToken cancellationToken)

{

    Exception? lastException = null;

    for (int attempt = 1;

        attempt <= MaxRetries;

        attempt++)

    {

        try

        {

            await _transport.SendBlockAsync(

                blockIndex,

                blockData,

                cancellationToken);

            return;

        }

        catch (OperationCanceledException)

        {

            throw;

        }

        catch (Exception ex)

        {

            lastException = ex;

            if (attempt < MaxRetries)

            {

                await Task.Delay(

                    200,

                    cancellationToken);

            }

        }

    }

    throw new InvalidOperationException(

        $"数据块 {blockIndex} 发送失败。",

        lastException);

}

}

这个服务按照数据块编号依次发送固件。

只有当前数据块得到设备确认后,才会继续发送下一块。如果发送失败,会进行有限次数的重试。

当所有数据块发送完成后,再通知设备校验固件并重启。

五、显示升级进度和处理取消

升级过程中,通常需要在界面上显示进度。

可以订阅 ProgressChanged 事件:

var upgradeService =

new OtaUpgradeService(otaTransport);

upgradeService.ProgressChanged += progress =>

{

Console.WriteLine(

$"当前升级进度:{progress}%");

};

开始升级:

var cancelSource =

new CancellationTokenSource();

try

{

await upgradeService.UpgradeAsync(

firmwarePath,

cancelSource.Token);

Console.WriteLine("固件升级完成。");

}

catch (OperationCanceledException)

{

Console.WriteLine("用户取消了固件升级。");

}

catch (Exception ex)

{

Console.WriteLine(

$"固件升级失败:{ex.Message}");

}

如果是桌面程序,可以把 progress 更新到进度条。

取消升级时,不能只停止 C# 程序中的发送任务。最好再向设备发送取消升级指令,让设备退出升级模式并清理临时固件。

如果设备正在执行固件写入,是否允许取消需要根据设备设计决定。有些阶段可以安全取消,有些阶段必须等待写入完成,否则可能破坏固件分区。

六、实际项目中需要注意的问题

首先,固件版本必须进行检查。

升级前应该读取设备当前版本,并判断新固件是否适用于这个设备。不同型号或不同硬件版本使用错误固件,可能导致设备无法启动。

其次,不能只依赖普通哈希值保证安全。

SHA256 可以检查文件是否完整,但不能证明固件来自可信来源。对安全要求较高的设备,还应该使用数字签名,并在设备端验证签名。

再次,要考虑断电和网络中断。

设备不应该直接覆盖当前正在运行的固件。比较可靠的方案是准备两个固件分区,新固件校验成功后再切换启动分区。如果升级失败,设备仍然可以回退到旧版本。

对于较大的固件,可以增加断点续传功能。设备记录最后一个成功写入的数据块编号,重新连接后,C# 程序从该编号继续发送,不必重新传输整个文件。

升级完成并重启后,C# 程序还应该重新连接设备,读取实际运行版本。只有设备成功启动并返回新版本号,整个 OTA 流程才算真正完成。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容