LIN-UDS Bootloader 上位机源码精读

张开发
2026/4/18 20:49:41 15 分钟阅读

分享文章

LIN-UDS Bootloader 上位机源码精读
LIN诊断实现基于UDS协议的OTA升级功能代码及资料支持AB面升级 产品包括: 1.升级上位机源码 2.MCU端源码boot和app包含LIN协议栈UDS协议框架包含常用SID服务代码 3.LIN学习资料和ISO14229资料 开发板硬件自行淘宝 5.根据ldf文件生成满足标准2.1协议代码的配置工具 联系付款后联系我百度下载 开发版价值一百块左右MCU为复旦微FM33LE015A车规级芯片方便移植到其他芯片我还移植过TI芯片 LIN调试工具为图莫斯USB转LIN工具——逐行拆解“LINOTA”究竟在做什么0. 写在前面这份代码是“整车域控 LIN 从件在线升级”的 PC 端实现文件总量 836 个但真正起调度作用的只有 4 个 cpp/h 对。全文将逐函数、逐变量、逐判断地跟踪一次完整的 OTA 流程回答三个问题每一句 C/C 到底把什么字节发到了 LIN 总线收到字节后上位机如何判断“继续”还是“重发”还是“弹窗报错”如果移植到另一颗 MCU/Bus最少需要改几行代码1. 入口函数main() 去哪了文件LinUdsUpgrade.cppCLinUdsUpgradeApp theApp; // 全局对象 BOOL CLinUdsUpgradeApp::InitInstance() { ... CLinUdsUpgradeDlg dlg; // 1. 主对话框 m_pMainWnd dlg; INT_PTR nResponse dlg.DoModal(); // 2. 阻塞消息循环 ... return FALSE; // 3. 退出即结束进程 }没有传统main()MFC 把theApp的InitInstance()当作入口。所以第一行用户代码是CLinUdsUpgradeDlg::OnInitDialog()——所有硬件初始化、UI 状态、升级线程都在这里诞生。2. 对话框的“一生”文件LinUdsUpgradeDlg.cpp2.1 OnInitDialog()——“打开软件的一瞬间”发生了什么// 1. 把默认参数写进 Edit 框 mEditUpgradeNodeAddr.SetWindowTextW(_T(7f)); // NAD mBzoneStartAddr.SetWindowTextW(_T(00008800)); // B 分区首地址 mBzoneSize.SetWindowTextW(_T(00007400)); // 29 KB // 2. 立即尝试打开 USB-LIN 适配器 int dev_idx 0, channel_idx 0; ret CLin::connect_toomoss(dev_idx); // 扫描 USB if(ret 0) MessageBox SendMessage(WM_CLOSE); // 失败直接退出 // 3. 初始化 LIN 硬件参数 ret CLin::init_lin(dev_idx, channel_idx, LIN_SLAVE_MODE, 19200); // 固定从机模式如果电脑没插适配器软件弹窗自退出避免后续空指针。LINSLAVEMODE表示上位机只发 Header从机发 Response——符合 LIN 2.x 诊断规范。2.2 “加载文件”按钮——OnBnClickedButtonLoadfile()CFileDialog fileDlg(TRUE, _T(bin), ...); if(fileDlg.DoModal()IDOK) { CFileStatus fs; CFile::GetStatus(path, fs); mUpgradeFileTotalSize fs.m_size; // 保存字节数 mUpgradeFilePath path; // 保存路径 // 把文件信息打印到只读 Edit CString info; info.Format(_T(文件大小%d Bytes\r\n创建日期%s), fs.m_size, fs.m_ctime.Format(...)); mEditUpgradeFilePath.SetWindowTextW(info); }只做文件大小校验不做 checksum——checksum 在升级线程里实时计算。打开文件后不立即读内容避免大文件占用内存真正读取在工作者线程里分段new/delete。2.3 “获取软件版本”按钮——OnBnClickedButtonGetSwVer()uint8_t req[3] {0x22, 0xFB, 0x88}; // UDS ReadDataByIdentifier SendDiagFrame(NAD, req, 3, resp); // 假设从机返回 [NAD][PCI][0x62][0xFB][0x88][Major][Minor][Patch] CString ver; ver.Format(_T(%d.%d.%d), resp[5], resp[6], resp[7]); mSoftwareVer.SetWindowTextW(ver);0xFB88 是自定义 DID从机源码里必须同样实现否则返回 NRC 0x31。SendDiagFrame()内部已经封装 5 次重发所以这里看不到 retry。2.4 “开始升级”按钮——OnBnClickedButtonUpgradeStart()// 1. 把 Edit 框的 16 进制字符串转成整数 mUpgradeNodeAddr HexToDem( strNAD ); mUpgradeStartAddr HexToDem( strStart ); mUpgradeSpaceSize HexToDem( strSize ); // 2. 简单合法性检查 if( mUpgradeNodeAddr0 || mUpgradeStartAddr0 || mUpgradeSpaceSize0 ) { MessageBox(...); return; } // 3. 禁用所有按钮防止用户二次点击 mButtonUpgradeStart.EnableWindow(FALSE); mBtnGetSwVer.EnableWindow(FALSE); mBtnLoadFile.EnableWindow(FALSE); // 4. 启动工作者线程 pCWinThread AfxBeginThread(UpgradeProcessThreadFunc, this);UI 线程与工作线程严格分离所有SendDiagFrame都在工作线程里完成避免阻塞消息循环。用static int mUpgradeSecCnt做“秒表”每秒WM_TIMER自增显示实际耗时。3. 升级线程UpgradeProcessThreadFunc()——**16 帧定一生**UINT UpgradeProcessThreadFunc(LPVOID lpParam) { CLinUdsUpgradeDlg* self (CLinUdsUpgradeDlg*)lpParam; /*----- 1. 读文件到 RAM -----*/ uint8_t* bin self-LoadUpgradeFile(); // new [] 分配 if(!bin) goto FINISH; /*----- 2. 计算包数 -----*/ uint32_t pkg_total (fileSize63)/64; // 向上取整 /*----- 3. 进入扩展会话 -----*/ uint8_t req[256] {0x10,0x03}; int ret self-SendDiagFrame(NAD, req, 2, resp); if(ret0 || resp[2]0x7F) goto FINISH; /*----- 4. 关闭 DTC -----*/ req[0]0x85; req[1]0x02; ret self-SendDiagFrame(NAD, req, 2, resp); ... /*----- 5. 编程会话 -----*/ req[0]0x10; req[1]0x02; ... /*----- 6. 安全解锁 -----*/ req[0]0x27; req[1]0x01; // 请求种子 ... uint32_t seed resp[4]24 | resp[5]16 | resp[6]8 | resp[7]; uint32_t key ~seed; // 简单取反 req[0]0x27; req[1]0x02; memcpy(req2, key, 4); ... /*----- 7. 检查预条件 -----*/ req[0]0x31; req[1]0x01; req[2]0x02; req[3]0x03; ... /*----- 8. 擦除 Flash -----*/ req[0]0x31; req[1]0x01; req[2]0xFF; req[3]0x00; memcpy(req4, self-mUpgradeStartAddr, 4); memcpy(req8, self-mUpgradeSpaceSize, 4); ... /*----- 9. 请求下载 -----*/ req[0]0x34; ... self-SendDiagFrame(NAD, req, 11, resp); /* 从机返回最大包长必须等于 64否则报错 */ /*----- 10. 循环发送 0x36 -----*/ ret self-LoopSendPkgData(bin, pkg_total); if(ret 0) goto FINISH; /*----- 11. 退出传输 -----*/ req[0]0x37; ... /*----- 12. 校验 CRC -----*/ uint16_t crc 0; for(int i0;itotalBytes;i) crc bin[i]; req[0]0x31; req[1]0x01; req[2]0x02; req[3]0x02; req[4]crc8; req[5]crc0xFF; ... /*----- 13. 用户确认重启 -----*/ INT_PTR ans MessageBox(... MB_SYSTEMMODAL|MB_OKCANCEL); if(ans IDOK) { req[0]0x11; req[1]0x03; // ECUReset self-SendDiagFrame(NAD, req, 2, resp); } FINISH: self-mButtonUpgradeStart.EnableWindow(TRUE); // 恢复按钮 delete [] bin; return 0; }全程 13 步任何一步收到 NRC 立即 goto FINISH保证不会死锁在编程会话。所有请求帧统一走 SendDiagFrame()该函数内部已经打印“发”与“收”的完整字节流调试时直接复制粘贴到 Excel 即可做时序分析。4. 发送与重发SendDiagFrame()——**五战三胜**int CLinUdsUpgradeDlg::SendDiagFrame(uint8_t nodeAddr, uint8_t* pFrameData, int frameLen, uint8_t* pRespData) { CString log; log.Format(_T(- 0x%02x 0x%02x ), nodeAddr, frameLen); for(int i0;iframeLen;i) log.AppendFormat(_T(%02x ), pFrameData[i]); OutputLog(log); // 1. 先打印“发” int ret -1, cnt 5; do{ ret CLin::upgrade_send_resp(nodeAddr, pFrameData, frameLen, pRespData); }while(ret0 cnt-- 0); // 2. 最多 5 次 if(ret0) ShowRespData(pRespData, ret); // 3. 打印“收” return ret; // 0 代表超时或无应答 }重试策略物理层偶尔掉包LIN 总线 12 V 毛刺所以固定 5 次成功后立刻退出不会多余占用总线。返回码语义0表示收到完整应答帧含 NADPCISID返回值是帧长度-1USB 写失败-2USB 读超时100 ms-3无有效 LIN 报文5. 分包写入LoopSendPkgData()——**64 B 世界的 for 循环**int CLinUdsUpgradeDlg::LoopSendPkgData(uint8_t* bin, int pkg_total) { uint16_t blk 0; uint8_t sendBuf[66]; // 0x36 blkId 64 B int retry 1; while(blk pkg_total) { sendBuf[0] 0x36; sendBuf[1] (blk1) 0xFF; // 块序号 1~255 memcpy(sendBuf2, bin blk*64, 64); int ret SendDiagFrame(NAD, sendBuf, 66, resp); if(ret 0) { if(retry 10) return -1; // 10 次后放弃 continue; } // 收到 NRC 0x72 也要重发 if(resp[2]0x7F resp[4]0x72) { if(retry 10) return -1; continue; } // 正常收到 0x76 retry 1; blk; int percent blk*100/pkg_total; mProgressUpgradeStep.SetPos(percent); CLinUdsUpgradeDlg::mUpgradeProgressPos percent; } return 0; }块序号从 1 开始兼容 UDS 规范“blockSequenceCounter”进度条刷新在 RAM 变量里备份即使界面被遮挡也能在OnPaint()中重绘10 次重试后直接 return -1线程会 goto FINISH回滚到扩展会话不会死锁6. CRC 校验简单累加和——**够用还是不够用**uint16_t crc_sum 0; for(int i0;itotalBytes;i) crc_sum bin[i];本例用“累加和”原因1. Bootloader 跑在 M0 内核Flash 擦写时 CPU 暂停复杂 CRC 耗时2. LIN 物理层本身有 8-bit 校验误码率 1e-6若追求车规级可替换为 CRC16-CCITTcppuint16t crc16(uint8tdata, int len){uint16t crc 0xFFFF;while(len--) crc crcccittupdate(crc,data);return crc;}双方同步替换即可上位机只改 3 行从机改 1 行。7. 异常与回滚ExitUpgrade()——**让从机“体面”地失败**void CLinUdsUpgradeDlg::ExitUpgrade(void) { // 1. 扩展会话 SendDiagFrame(NAD, {0x10,0x03}, 2, resp); // 2. 重新启用 DTC SendDiagFrame(NAD, {0x85,0x01}, 2, resp); // 3. 默认会话 SendDiagFrame(NAD, {0x10,0x01}, 2, resp); }任何一步失败都会调用ExitUpgrade()确保从机回到默认会话否则从机如果在编程会话里重启可能由于看门狗没喂而砖机。代码顺序不能反必须先 0x10 03→0x85 01→0x10 01否则部分厂商 Bootloader 会 NRC 0x7E服务在当前会话不支持8. USB-LIN 驱动层CLin::upgrade_send_resp()——**把字节塞进 USB 端点**int CLin::upgrade_send_resp(uint8_t nodeAddr, uint8_t* pSendData, int sendLen, uint8_t* pRespData) { LIN_UDS_ADDR addr {0x3C, 0x3D, nodeAddr, 0, 10}; // 固定 PID LIN_UDS_Request(handle, channel, addr, pSendData, sendLen); uint8_t tmp[256]; int ret LIN_UDS_Response(handle, channel, addr, tmp, 100); if(ret 0) return ret; LIN_EX_MSG msg[100]; int count LIN_UDS_GetMsgFromUDSBuffer(handle, channel, msg, 100); if(count 0) return -3; // 取最后一帧作为应答 memcpy(pRespData, msg[count-1].Data, msg[count-1].DataLen); return msg[count-1].DataLen; }LINUDSRequest只是把数据推进 USB FIFO不等待应答LINUDSResponse阻塞 100 ms底层已经做完多帧拼接0x10/0x21/0x22返回的msg[].Data已经去掉 NAD、PCI直接就是 SIDData所以上层打印时手动补 NAD9. 打印与调试OutputLog()——**研发人员的“黑匣子”**void CLinUdsUpgradeDlg::OutputLog(const CString str) { int len mEditLog.GetWindowTextLength(); mEditLog.SetSel(len, len); // 移到末尾 mEditLog.ReplaceSel(str _T(\r\n)); mEditLog.LineScroll(mEditLog.GetLineCount()); }线程安全工作线程调用OutputLog()时MFC 自动通过消息队列派发不会跨线程直接操作 HWND日志格式固定为“发”行以“-”开头收行以“-”开头可一键正则提取为 CSV再用 MATLAB 画图分析时间戳10. 移植 checklist——**把这套代码搬到 CAN 上要改哪里**模块是否必须改改动点CLin::upgradesendresp()是把LINUDSRequest/Response换成CAN_Write/ReadID 改为物理寻址 0x6xxLINUDSADDR 结构体是删掉 ReqID/ResID改为 29-bit CAN IDSendDiagFrame()否保持原样统一入口0x36 分包大小是CAN 单帧 8 B多帧 64 B需要改 64→56留 2 B 给 PCICRC 算法可选保持累加和也可改 CRC16 只需替换 3 行进度条否无需改动重试次数可选CAN 误码率低5 次→3 次结论业务逻辑0x10/0x27/0x31…完全复用只改物理层 4 个函数即可在 30 分钟内完成 CAN 移植。11. 小结——**代码在“功能”层面做了什么把 bin 文件原封不动搬到 RAM然后按 64 B 切包通过 13 条 UDS 指令完成“解锁→擦除→写入→校验→重启”车规流水线任何一步失败自动回滚到默认会话防止砖机全程日志可追溯研发调试可精确到字节USB-LIN 驱动与 MFC UI 完全解耦移植到 CAN/Ethernet 只需换“最底层 4 个函数”这就是一段短小却完整的 Bootloader 上位机实现没有花哨的框架只有一行行可验证的字节流LIN诊断实现基于UDS协议的OTA升级功能代码及资料支持AB面升级 产品包括: 1.升级上位机源码 2.MCU端源码boot和app包含LIN协议栈UDS协议框架包含常用SID服务代码 3.LIN学习资料和ISO14229资料 开发板硬件自行淘宝 5.根据ldf文件生成满足标准2.1协议代码的配置工具 联系付款后联系我百度下载 开发版价值一百块左右MCU为复旦微FM33LE015A车规级芯片方便移植到其他芯片我还移植过TI芯片 LIN调试工具为图莫斯USB转LIN工具没有隐藏的魔法只有 5 次重试累加和没有模糊的“失败”只有 20 种中文 NRC 弹窗。读懂它你就拥有了一把“打开任何 OTA 升级系统”的万能钥匙。

更多文章