CRC校验 | 程序如何检查自身完整性?

原创 鱼鹰谈单片机 2021-08-08 21:26

来源:公众号【鱼鹰谈单片机】

作者:鱼鹰Osprey

ID   :emOsprey


在一些比较严格的行业里面,不是说你的程序能完成必要功能就可以,还需要添加一些额外的功能,比如最常见的看门狗功能,它可以在程序死机时完成重启,但也仅仅如此而已。很多异常它是无法检查的,比如程序偶然跑飞,ram 异常、flash异常等其他问题,只有程序hardfault或者其他严重问题导致程无法喂狗时才能起作用。
所以有些产品为了保障安全,会增加安规代码,保证程序能够正常运行(UL/CSA/IEC 60730-1/60335-1 B类认证)。

自检内容

MCU 安全检查一般包括以下几个方面:

1、CPU 自测(寄存器测试)
2、系统时钟频率测量(保证时钟正常工作,不快也不慢,GD 芯片在短路晶振后,程序暂停运行,无法检查,但是 ST 芯片会自动切换到内部时钟,可以由程序检查这种异常)
3、RAM 自检
4、FLASH 存储器完整性检查
5、独立看门狗、窗口看门狗检查
6、安全相关变量检查
7、中断检查
8、I/O 口检查
9、栈检查
10、程序流程控制
11、AD 口检查

你会发现真要完成这份安规代码,难度不是一般的大,不过一般芯片厂商会提供相关参考例程和相关文档,但不是说有了这些资料就完全没有问题了。

比如 ST 提供了一个参考例子,但是它使用的 HAL 库(事实上它还有标准库,当时不知道),如果原本程序用的标准库,那么就需要进行移植,这个工作量也不是一般大(首先要能理解程序,才能进行正确移植,而里面的逻辑还是很复杂的)。如果你不想移植,还有一个办法是使用 lib 库,就是将相关功能打包成一个库,虽然程序会大一些(毕竟很多底层代码和原来的重复了),但确实是比较简单的方法(前提是 flash 够大)。

鱼鹰走的是第一条路,移植,并且将相关的底层代码提供了接口,这样不管是用标准库还是 HAL 库,只要自己实现这这些特定的接口即可完成。

另外,参考例子只是实现了一个最基本的功能,在真正的产品不一定能适用。比如你的程序负载大,而里面为了测量时钟频率,几百微秒时间就要进入一次中断(即使是分频后),如果刚好在中断产生时,其他程序禁用了中断,运行这些代码有可能就会出现问题,很容易错过中断而导致复位。

在我一开始移植的时候就是如此,在一个简单的程序里面可以正常运行很长时间,但是移植到产品工程里面,时不时出现时钟检查不通过的时候,导致程序不停重启,最终鱼鹰通过 DMA 传输的方式解决了这个问题,再也不会因为时钟检查不通过导致重启了。

另外一个难点是对 .sct (分散加载)文件的理解,这个会在后面介绍。

安规相关的内容实在是太多,要写的话可以写成一个系列了,如果各位道友感兴趣的话,多多转发支持一下鱼鹰,如果效果不错,鱼鹰会考虑完成后续的其它部分。(这里有一份比较全面但简单一些的参考文章可以看看 http://news.eeworld.com.cn/mp/STM32/a80041.jspx,只介绍如何做,没怎么介绍为什么这么做)

资料

ST 相关资料可以查看以下内容(www.st.com,下载时需要注册邮箱才行,鱼鹰公众号后台提供了部分资料,可自行领取)

《AN4435 应用笔记》中文版,《AN277》(ROM Self-Test)
STM8-SafeCLASSB
https://www.st.com/en/embedded-software/stm8-safeclassb.html

STM32-CLASSB-SPL(基于标准外设库)
https://www.st.com/en/embedded-software/stm32-classb-spl.html#tools-software

X-CUBE-CLASSB(基于HAL库)
https://www.st.com/en/embedded-software/x-cube-classb.html(不同版本有不同芯片,比如 2.2.0 版本的是 Fx 相关的,2.3.0 是H7、G0 相关的)

当然国产芯片也一般会提供例程。

本篇笔记只介绍其中一个内容,即 FLASH 检查,换句话说就是程序完整性检查。

FLASH 检查

我们以比较复杂的 boot + app + rtos ,开发环境 keil 、stm32f103 为例介绍相关知识。

一般 boot 和 app 部分是用不同工程管理的,所以 app 部分代码只能检查自身的完整性,而不能检查 boot 部分。

并且 app 的 flash 区也不是完全检查的,有一小部分是也没法检查的,但这并不影响它的功能(既然已经跳转到 app 里面了,那么 boot 部分 flash 即使在运行时有问题也不影响功能,而如果变量初始值的flash有问题就是关键变量检查的问题了)。

现在就是如何检查的问题了。

如何检查 | 基本原理

校验手段有很多,比如 和校验、MD5 校验、CRC 校验,这里我们使用 CRC,因为一般芯片内部会内置该外设硬件计算(如果没有,可以纯 CPU 计算)。

然后我们需要了解完整性检查的基本原理。

所谓程序完整性检查,就是在下载代码前,先用工具把要校验的部分通过计算公式计算出一个值,保存在某个地方(flash),然后程序在运行的时候,自己也去读取要校验的 flash 部分,通过同样的计算公式计算出一个值,然后将这个值和保存在 flash 里面的值进行比较,就可以看出代码是否存在异常了,有异常及时处理,没有异常就继续重新检查。

而检查分成两个步骤:

1、开机时,一次性完成所有计算,保证运行前完整。

2、正常运行时,定时计算,每次计算一个小块,当计算完最后一块时才比较结果,成功就重新继续计算,失败则终止程序运行,周而往复(计算需要较长的时间,分时计算可以不影响程序正常功能),这样可以保证程序在运行时也能检查 FLASH 的完整性,防止 FLASH 运行过程中破坏掉。

现在有个问题,CRC 保存在何处才是合适的?

随便保存在一个地方肯定是不行的。假设这个位置在要校验代码部分的里面,那么当工具计算这个值时,又会篡改掉校验部分里面的数据(因为你把 CRC 值放到里面了),那么你的程序校验时,肯定不通过,因为你读了一个被改变的 CRC 值。所以这个值一定要放在代码的最后面才行。

另外前面说过,运行时会一小块一小块,所以要保证你的 CRC 值存放位置应该在小块大小的边界位置上。比如一次计算 16 字节,那你存放的位置应该是 16 的倍数才是正常的。

所以,CRC 存放位置存在这两个限制。

另外,如何提前计算好 CRC 的值呢?IAR 内置该功能,而 KEIL 我们可以借助强大的开源工具 SRecord《功能强大的 HEX 开源转换工具,你值得拥有》(一转眼,这篇文章差不多鸽了四个多月了)帮助我们计算。

基本知识都了解的差不多了,接下来就是如何操作的问题。

实操

1、固定 CRC 位置。

我们可以在启动文件的最后加入以下代码(END 前

这里默认是 0x3D334398,但会在后续修改成正确的 CRC 值

;*******************************************************************************; User Checksum - must be placed at the end of memory;*******************************************************************************                AREA    CHECKSUM, DATA, READONLY, ALIGN=6                EXPORT  __Check_Sum
; Alignement here must correspond to the size of tested block at FLASH run time test (16 words ~ 64 bytes)!!! ALIGN
__Check_Sum DCD 0x3D334398; ; Check sum computed externaly

这里保证了 __Check_Sum 的地址是 2 ^ 6 大小对齐,所以你的计算小块可以这个大小,当然也可以小一些,比如 2 ^ 5 等。这样就可以将检查部分分成固定的小块,不会多,也不会少,刚刚好(必须)。

那么如何将这个地址固定在代码最后呢?这个时候就需要我们的 .sct 文件发挥作用了(ClassB_stm32F10x.sct)。

ER_IROM1 0x08000000 0x10000  {  ; load address = execution address      *.o (RESET, +First)      *(InRoot$$Sections)      .ANY (+RO)      *.o (CHECKSUM, +Last) ;放置在最后    }

我们用了 +Last 将其放置在代码的最后部分,你想把它放置在 bin 文件最后面?暂时鱼鹰还没想到怎么做,有知道的道友可以告诉鱼鹰(通过 sct 的方式)。

2、CRC 计算脚本
在 windows 叫批处理,.bat ,我们可以在参考例程中找到。crc_gen_keil.bat

我们需要需改三个位置

第一个是你的计算工具的路径,里面应该要有计算工具。

第二个就是你的工程名字,我们通过下面位置确定(鱼鹰用的 Main):

最后是工程路径。一般在 Objects 文件夹里面,而 map 文件一般在 Listings 文件夹里面。

说白了,这些变量就是为了让脚本能够找到 map、hex 文件和工具。但一般默认工程,这两个文件可能不在一个文件夹里面,所以我们可以对例子中的批处理文件 crc_gen_keil.bat 进行适当修改

map 文件的作用是为了让脚本能够搜索到 __Check_Sum 的地址,然后就可以计算 CRC 并修改 HEX 里面这个值了。

另外还有新增了一个变量 HEX_ADRR,当我们的计算位置不是从 0x08000000 开始时(比如 app 起始地址在 0x08009000),我们就可以修改这个变量值。还有我们希望在计算完并修改 CRC 后可以自己生成 bin 文件方便我们更新固件,还需要加入转化成 bin 的命令。

其中为了下载修改(CRC)后的 HEX 文件,我们还需要简单修改一下,用于判断工具是否存在,不存在,直接删除 hex 和 axf 文件(防止下载未修改的文件)。
%xxx% 类似脚本中的 $xxx

if not exist %SREC_PATH% (    echo %SREC_PATH% is not exit, exit    echo ----------------------------------------del %INPUT_HEX% -- %AXF_FILE% ---------------    del %INPUT_HEX% %AXF_FILE%    exit)

这样可以保证,一定能够正确下载 HEX 文件,而不是下载默认的 axf 文件。
否则,下载的默认 axf 文件会因为 CRC 未修改,
程序将不断重启
完整的修改(可以自行对比官方例程文件):

@echo offECHO Computing CRCECHO -------------------------------------REM Batch script for generating CRC in KEIL projectREM Must be placed at MDK-ARM folder (project folder)
REM Path configurationSET SREC_PATH=C:\SRECSET MAP_NAME=STM3210C_EVALSET MAP_PATH=STM3210C_EVALSET TARGET_NAME=STM3210C_EVALSET TARGET_PATH=STM3210C_EVALSET BYTE_SWAP=1SET COMPARE_HEX=1SET CRC_ADDR_FROM_MAP=1REM Not used when CRC_ADDR_FROM_MAP=1SET CRC_ADDR=0x08007ce0
REM Derived configurationSET HEX_ADRR=0x08000000SET MAP_FILE=%MAP_PATH%\%MAP_NAME%.mapSET AXF_FILE=%TARGET_PATH%\%MAP_NAME%.axfSET INPUT_HEX=%TARGET_PATH%\%TARGET_NAME%.hexSET OUTPUT_HEX=%TARGET_PATH%\%TARGET_NAME%_CRC.hexSET OUTPUT_BIN=.\%TARGET_NAME%_CRC.binSET TMP_FILE=crc_tmp_file.txt
if not exist %SREC_PATH%\srec_cat.exe ( echo %SREC_PATH% is not exit, exit echo ----------------------------------------del %INPUT_HEX% -- %AXF_FILE% --------------- del %INPUT_HEX% %AXF_FILE% exit)
IF NOT "%CRC_ADDR_FROM_MAP%"=="1" goto:end_of_map_extractionREM Extract CRC address from MAP fileREM -----------------------------------------------------------REM Load line with checksum location to crc_search variableECHO Extracting CRC address from MAP fileFINDSTR /R /C:"^ *CHECKSUM" %MAP_FILE%>%TMP_FILE%SET /p crc_search=<%TMP_FILE%DEL %TMP_FILE%REM remove '(' character and string after, which causes errorsfor /f "tokens=1 delims=(" %%a in ("%crc_search%") do set crc_search=%%aREM remove CHECKSUM string from variableSET crc_search=%crc_search:CHECKSUM=%REM get first word at line, which should be CRC address in HEX formatfor /f "tokens=1 delims= " %%a in ("%crc_search%") do set CRC_ADDR=%%aREM -----------------------------------------------------------REM End of CRC address extraction:end_of_map_extraction
REM Compute CRC and store it to new HEX fileECHO CRC address: %CRC_ADDR%if "%BYTE_SWAP%"=="1" (REM ECHO to see what is going onECHO %SREC_PATH%\srec_cat.exe ^ %INPUT_HEX% -intel ^ -crop %HEX_ADRR% %CRC_ADDR% ^ -byte_swap 4 ^ -stm32-b-e %CRC_ADDR% ^ -byte_swap 4 ^ -o %TMP_FILE% -intel %SREC_PATH%\srec_cat.exe ^ %INPUT_HEX% -intel ^ -crop %HEX_ADRR% %CRC_ADDR% ^ -byte_swap 4 ^ -stm32-b-e %CRC_ADDR% ^ -byte_swap 4 ^ -o %TMP_FILE% -intel ) else (REM ECHO to see what is going onECHO %SREC_PATH%\srec_cat.exe ^ %INPUT_HEX% -intel ^ -crop %HEX_ADRR% %CRC_ADDR% ^ -stm32-l-e %CRC_ADDR% ^ -o %TMP_FILE% -intel%SREC_PATH%\srec_cat.exe ^ %INPUT_HEX% -intel ^ -crop %HEX_ADRR% %CRC_ADDR% ^ -stm32-l-e %CRC_ADDR% ^ -o %TMP_FILE% -intel)ECHO %SREC_PATH%\srec_cat.exe ^ %INPUT_HEX% -intel -exclude -within %TMP_FILE% -intel ^ %TMP_FILE% -intel ^ -o %OUTPUT_HEX% -intel%SREC_PATH%\srec_cat.exe ^ %INPUT_HEX% -intel -exclude -within %TMP_FILE% -intel ^ %TMP_FILE% -intel ^ -o %OUTPUT_HEX% -intelREM Delete temporary fileDEL %TMP_FILE%ECHO Modified HEX file with CRC stored at %OUTPUT_HEX%
REM Compare input HEX file with output HEX fileif "%COMPARE_HEX%"=="1" (ECHO Comparing %INPUT_HEX% with %OUTPUT_HEX%%SREC_PATH%\srec_cmp.exe ^ %INPUT_HEX% -intel %OUTPUT_HEX% -intel -v)
del %INPUT_HEX%
ECHO %SREC_PATH%\srec_cat.exe ^ %OUTPUT_HEX% -intel -offset -%HEX_ADRR% -o %OUTPUT_BIN% -binary%SREC_PATH%\srec_cat.exe ^ %OUTPUT_HEX% -intel -offset -%HEX_ADRR% -o %OUTPUT_BIN% -binary
ECHO -------------------------------------

3、 CRC 计算部分代码(摘自官方例程)
完整计算


分小块计算

需要注意的是,每次全部检查完之后得复位一下 CRC 外设,否则会继续用之前的结果继续计算。

4、工程配置
准备好前面的内容后,即可进行工程配置。
生成 HEX

使用 debug 按钮时下载的文件:
crc_load.ini (需要根据自己的工程自行修改)

特别注意里面的双反斜杠,没有它,将找不到正确路径。这里以工程文件(.uvprojx)所在路径为相对路径。

使用 load 按钮时下载配置:

不然你下载(点击 load)的时候,就会下载默认的 axf 文件,而 axf 里面的 CRC 值也是默认的,并没有被修改,所以这一步也是必须的。

使用修改的分散加载文件,这可以保证我们的 CRC 存放位置在代码最后面。

最后一步,当编译完成后,让工具帮我们自动计算 CRC 值,并将值修改到 HEX 文件里面。

添加我们前面的批处理文件:

这样所有的工程配置就完成了。

效果

我们可以看看效果。

首先,我们并没有添加工具,我们可以看到,脚本自动退出了,并且删除了 hex 文件和 axf 文件,这样就不会下载错误的 HEX 文件了(点击下载会发现找不到 axf 文件)。

当我们在 C 盘添加工具后编译:

从这里我们可以得到几点信息:
1、计算范围 0x08000000 ~ 0x08007640。
2、CRC 存放位置在 0x08007640,四个字节
3、可以使用 srec_cmp.exe 比较两个 HEX 文件的区别(修改前和修改后)。这里的区别在 0x08007640 ~ 0x8007643。
4、生成的 bin 文件和 hex 文件相对存放路径。

大功告成!

工具命令解释

现在我们可以从这里了解到三个命令。

C:\SREC\srec_cat.exe   STM3210C_EVAL\STM3210C_EVAL.hex -intel   -crop 0x08000000 0x08007640   -byte_swap 4   -stm32-b-e 0x08007640   -byte_swap 4   -o crc_tmp_file.txt -intel

这个命令用于截取 0x08000000~0x08007640 的内容并计算 CRC 值,并且在 0x08007640 位置处写入 CRC 值。0x08007640 由 map 文件得出,即 __Check_Sum 的地址。

C:\SREC\srec_cat.exe STM3210C_EVAL\STM3210C_EVAL.hex -intel -exclude -within crc_tmp_file.txt -intel   crc_tmp_file.txt -intel -o STM3210C_EVAL\STM3210C_EVAL_CRC.hex -intel

该命令用于将两个 HEX 文件合并,如果以 crc_tmp_file.txt 文件为基准,即同一个地址的值如果不同,则保留 crc_tmp_file.txt 里面的(里面有正确的 CRC),-intel 代表 HEX 文件类型。

C:\SREC\srec_cmp.exe   STM3210C_EVAL\STM3210C_EVAL.hex -intel STM3210C_EVAL\STM3210C_EVAL_CRC.hex -intel -v

终于搞定啦,可以放下这个了。

如果对你有帮助,欢迎转发支持鱼鹰

鱼鹰谈单片机 面向软件开发进阶读者,分享包括但不限于 C 语言、KEIL、STM32、51 等知识!
评论
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 198浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 260浏览
  • 前篇文章中『服务器散热效能不佳有解吗?』提到气冷式的服务器其散热效能对于系统稳定度是非常重要的关键因素,同时也说明了百佳泰对于散热效能能提供的协助与服务。本篇将为您延伸说明我们如何进行评估,同时也会举例在测试过程中发现的问题及改善后的数据。AI服务器的散热架构三大重点:GPU导风罩:尝试不同的GPU导风罩架构,用以集中服务器进风量,加强对GPU的降温效果。GPU托盘:改动GPU托盘架构,验证出风面积大小对GPU散热的影想程度。CPU导风罩:尝试封闭CPU导风罩间隙,集中风流,验证CPU降温效果。
    百佳泰测试实验室 2025-01-24 16:58 50浏览
  • 书接上回:【2022年终总结】阳光总在风雨后,启航2023-面包板社区  https://mbb.eet-china.com/blog/468701-438244.html 总结2019,松山湖有个欧洲小镇-面包板社区  https://mbb.eet-china.com/blog/468701-413397.html        2025年该是总结下2024年的喜怒哀乐,有个好的开始,才能更好的面对2025年即将
    liweicheng 2025-01-24 23:18 99浏览
  • 项目展示①正面、反面②左侧、右侧项目源码:https://mbb.eet-china.com/download/316656.html前言为什么想到要做这个小玩意呢,作为一个死宅,懒得看手机,但又想要抬头就能看见时间和天气信息,于是就做个这么个小东西,放在示波器上面正好(示波器外壳有个小槽,刚好可以卡住)功能主要有,获取国家气象局的天气信息,还有实时的温湿度,主控采用ESP32,所以后续还可以开放更多奇奇怪怪的功能,比如油价信息、股票信息之类的,反正能联网可操作性就大多了原理图、PCB、面板设计
    小恶魔owo 2025-01-25 22:09 85浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 192浏览
  • 随着AI大模型训练和推理对计算能力的需求呈指数级增长,AI数据中心的网络带宽需求大幅提升,推动了高速光模块的发展。光模块作为数据中心和高性能计算系统中的关键器件,主要用于提供高速和大容量的数据传输服务。 光模块提升带宽的方法有两种:1)提高每个通道的比特速率,如直接提升波特率,或者保持波特率不变,使用复杂的调制解调方式(如PAM4);2)增加通道数,如提升并行光纤数量,或采用波分复用(CWDM、LWDM)。按照传输模式,光模块可分为并行和波分两种类型,其中并行方案主要应用在中短距传输场景中成本
    hycsystembella 2025-01-25 17:24 45浏览
  • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
    虹科Pico汽车示波器 2025-01-23 10:46 131浏览
  • 飞凌嵌入式基于瑞芯微RK3562系列处理器打造的FET3562J-C全国产核心板,是一款专为工业自动化及消费类电子设备设计的产品,凭借其强大的功能和灵活性,自上市以来得到了各行业客户的广泛关注。本文将详细介绍如何启动并测试RK3562J处理器的MCU,通过实际操作步骤,帮助各位工程师朋友更好地了解这款芯片。1、RK3562J处理器概述RK3562J处理器采用了4*Cortex-A53@1.8GHz+Cortex-M0@200MHz架构。其中,4个Cortex-A53核心作为主要核心,负责处理复杂
    飞凌嵌入式 2025-01-24 11:21 151浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 999浏览
  • 不让汽车专美于前,近年来哈雷(Harley-Davidson)和本田(Honda)等大型重型机车大厂的旗下车款皆已陆续配备车载娱乐系统与语音助理,在路上也有越来越多的普通机车车主开始使用安全帽麦克风,在骑车时透过蓝牙连线执行语音搜寻地点导航、音乐播放控制或免持拨打接听电话等各种「机车语音助理」功能。客户背景与面临的挑战以本次分享的客户个案为例,该客户是一个跨国车用语音软件供货商,过往是与车厂合作开发前装车机为主,且有着多年的「汽车语音助理」产品经验。由于客户这次是首度跨足「机车语音助理」产品,因
    百佳泰测试实验室 2025-01-24 17:00 52浏览
我要评论
1
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦