news 2026/10/20 8:45:52

TMS320F28335模型化开发:基于Simulink的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TMS320F28335模型化开发:基于Simulink的完整实践

做电力电子和电机控制的工程师,对主控芯片DSP TMS320F28335应该都不陌生。这几年圈子里讨论最多的一个开发方式,就是基于Matlab Simulink做嵌入式模型,把控制算法从“手写C代码”变成“图形化模块搭好之后自动生成C代码”。不只是28335,整个C2000系列都在走这条路。很多人觉得模型化开发玄乎,其实它的核心逻辑很朴素:把最费脑袋的控制算法放在Simulink里做,硬件相关的工作仍然留在传统C代码里,最后用工具把两边合起来。这篇文章我就把整个链路拆开聊一遍,从为什么选这颗芯片,到模型怎么搭、配置怎么设、代码怎么生成、上板有哪些坑,一次讲透,适合刚准备入门的同学,也适合正在评估要不要转到模型化开发的朋友。

1. 为什么选F28335做主控芯片,为什么要用Simulink做模型开发

1.1 硬件资源盘点:这颗DSP到底强在哪

TMS320F28335是C2000家族里非常典型的一款浮点DSP,主频150MHz,带单精度浮点运算单元FPU,内部有256KB Flash和34KB SRAM。外设方面,12位ADC、6个ePWM模块、eQEP正交编码接口、eCAP捕获单元、CAN、SCI、SPI、McBSP、DMA,几乎把电机控制和数字电源需要的接口都集齐了。在F28335刚推出来的年代,这个配置在控制类芯片里是相当能打的,哪怕放到现在,它的外设组合和运算能力依然能满足大多数中等复杂度的控制算法。

很多人问,为什么不用ARM Cortex-M或者更新的C2000系列?我的看法是,F28335的成熟度太高了。一方面,市面上关于它的参考设计、例程、中文资料非常多,遇到问题基本都能搜到解决方案。另一方面,它在成本、性能、开发工具链之间找到了一个很均衡的点。尤其是浮点运算,这个优势在早期定点DSP面前几乎是一骑绝尘的。

做过早期C24x或者C28x定点芯片的人应该有印象,光是PI控制器里的系数就要用Q格式去做定点转换,稍微不注意就溢出或者精度丢失。每次调整算法参数,都得同步改一堆scale系数,极其痛苦。而F28335的FPU让PI、观测器、坐标变换这些东西可以直接用浮点数学表达式写出来,跟论文里的公式对得上,仿真和实际跑出来的结果差距小了很多,开发效率提升不是一点半点。

1.2 模型化开发到底解决了什么问题

传统手写C的流程是这样的:先看控制理论,再手动改成C代码,然后编译烧录,用示波器看波形,发现不对再回头改代码。问题在于,控制理论和代码实现之间隔着一层“翻译”,翻译错了很难发现,波形不对时你根本不知道是算法本身错了还是代码实现错了。比如PI的积分项符号反了、坐标变换的角度没有对齐、变量类型溢出,这些都是手写C里非常隐蔽的低级错误。

模型化开发把这一层“翻译”省掉了。在Simulink里搭的模型就是算法的最终形态,信号怎么流、增益怎么设、限幅加在哪,全部可视化。离线仿真的意义不只是看输出漂不漂亮,而是让你在写任何嵌入式代码之前,就已经把控制逻辑完整地验证了一遍。你可以在模型里加入采样延时、量化误差、饱和特性,模拟真实工况,找到问题并修正后再生成代码,上板之后的调试成本会低很多。

对于团队协作,模型化开发还有天然优势。控制工程师把算法模型搭好,嵌入式工程师把驱动和集成接好,两个人不需要互相等,也不需要读对方天书一样的代码。Simulink模型本身就是文档,比一份经常过时的设计文档靠谱得多。

1.3 什么时候该用模型化,什么时候别硬上

我必须诚实地说,模型化开发不是银弹,也不适合所有场合。如果项目只是做一个简单的状态机、Bootloader,或者对时序要求极其苛刻的定制外设协议,手写C反而更直接。Simulink生成的代码虽然经过优化,但函数调用层级和代码体积通常比手写C大,对于资源极度受限的场景可能不划算。

比较适合模型化的场景有三个特征:第一,算法复杂,比如FOC、并网逆变器、数字电源里的状态观测器;第二,需要频繁调参和迭代,模型可以快速仿真验证;第三,团队成员之间有明确的分工,算法和驱动可以并行开发。我见过有人把外设初始化也硬塞进Simulink里,结果生成出来的代码又大又难维护,完全没必要。正确的边界应该是:算法相关的部分用模型表达,底层硬件驱动、中断系统初始化、系统启动流程仍然用传统C代码。这也是我后面要详细说的分层思路。

2. 搭建可运行的Simulink嵌入式模型框架

2.1 模型分层:算法和硬件解耦是核心原则

我一开始接触模型化开发时,犯过一个典型错误——把所有东西都放在一个巨大模型里,ADC采样、PWM输出、控制算法、通讯协议全混在一起。结果模型乱到连自己都不想打开,想单独验证算法根本无从下手。后来参考了一些业内范例,才意识到模型也要分层,而且分层逻辑跟软件工程里MVC那套很像。

我习惯把嵌入式模型分成三层。最顶层是硬件访问层,包含ADC、PWM、Encoder等外设模块,负责把真实的模拟量转换成标幺值,把内部计算出来的占空比映射回寄存器值。中间层是控制算法层,包含坐标变换、PID调节器、SVPWM、磁链观测器这些纯数学逻辑。最底层是平台抽象层,处理中断、采样触发、时钟配置等全局事务。

这样分层最大的好处是,离线仿真时我可以把顶层外设模块替换成理想信号源,比如直接用一个正弦波发生器模拟电流传感器的输出,用常数模块代替母线电压采集值。这样算法模型可以在不依赖任何真实硬件的情况下独立运行,控制参数的初步整定在电脑上就能完成。真正上板时,再把真实外设模块接回去,生成代码直接部署。中间那部分算法模型几乎不用改动。

2.2 关键C2000硬件支持模块的配置逻辑

要在Simulink里针对F28335做嵌入式开发,第一步是安装Embedded Coder的C2000支持包,这个通常可以通过Matlab的Add-On Explorer直接安装。版本匹配问题非常关键,我踩过不少坑,这里把实际用过的匹配组合列一个表供参考。

Matlab版本CCS版本建议注意事项
R2018bCCS 8.x老版本,稳定但界面老旧
R2021bCCS 10.x广泛使用,资料多
R2023a/R2023bCCS 12.x新版,支持C2000全系列,需要较新Java环境

装好支持包之后,模型库里就会出现C28x相关的模块,常用的有ADC、PWM、QEP、SPI、CAN、中断等。ADC模块配置时,要把精度设为12位,触发源选择由ePWM事件触发,转换序列按需配置。一个典型的电机控制场景里,我会在同一个ADC模块里配置三路电流采样、一路母线电压采样,用Sequencer自动完成多通道排队转换,这样在一次PWM周期里就能拿到所有反馈值。

PWM模块是电机控制里的重中之重。载波频率、死区时间、互补输出模式、对齐方式都在这里配置。以10kHz载波为例,28335的ePWM时钟是150MHz,采用up-down计数模式(对称PWM),那么TBPRD寄存器值大约是7500,计算方式是150MHz除以20kHz,再减1。这里实际是2x10kHz,因为up-down模式下PWM频率等于TBCLK除以2倍TBPRD。死区时间按照驱动器的IGBT或MOSFET开关特性来定,我常用的IGBT模块配2us左右,MOSFET可以更短。

中断模块建议用Function-Call Subsystem来实现。简单说,就是从一个中断源(比如ADC中断或ePWM中断)引出触发信号,这个信号进入一个特殊的子系统,在里面放控制算法,每次中断到来时算法就执行一次。这样可以保证采样、算法运算、PWM占空比更新在同一个时间点完成,避免了异步运行带来的控制抖动。

2.3 从纯仿真模型迁移到嵌入式模型的第一步

很多人把Simulink里现成的仿真模型直接拿过来生成代码,结果一堆报错。原因很简单,仿真模型往往用了变步长连续求解器,还有连续积分器、传输延时这些在嵌入式实时环境里不存在的元素。迁移的第一步,就是把这些“仿真专属”的东西换掉。

Solver必须改成定步长离散求解器,比如discrete(no continuous states)。采样时间要跟实际控制周期对齐,比如10kHz控制频率对应的步长是0.0001秒。模型里所有连续积分器改成离散形式的离散积分器或者直接改用PID Controller模块。

还有一个容易忽略的点是信号的数据类型。仿真模型里默认是double,但生成到DSP上,如果某个信号最终要送进PWM寄存器,就得在链路末端加上Data Type Conversion模块,把double转成整数类型。否则仿真没问题,生成代码编译不过,或者生成出来的代码隐式截断,在线调试时完全找不着原因。

3. 完整实操:从模型到28335板卡的代码生成

3.1 代码生成的参数配置清单

代码生成前,有几个关键配置项必须逐项检查。首先是Solver面板,选择Fixed-step、discrete。其次是Hardware Implementation面板,设备供应商选择对应的C2000处理器系列,具体型号选TMS320F28335。这一步决定了生成代码里包含的寄存器头文件和编译器宏定义是否正确。

然后是Code Generation面板,系统目标文件选ert.tlc,也就是Embedded Coder目标。如果只是用通用Simulink Coder,生成的代码往往是仿真验证用的,不能直接部署到F28335上。嵌入式的ERT目标会生成更精简、更适合生产环境的代码。

还需要确认是否生成示例main文件。如果你打算把生成代码集成到已有CCS工程里,我建议把“Generate an example main program”关掉,只要模型源码文件就够了。如果是从零开始的新项目,留着这个示例main也无妨,可以快速跑通流程。

配置完成后,在模型界面里按Ctrl+B即可生成代码。生成结果通常包含模型名.c和.h文件、模型_initialize()函数、模型_step()函数,可能还有interrupt相关代码,取决于模型里是否用了中断模块。我这里给一个典型集成方式的伪代码,很多人看了会觉得,原来集成也没那么神秘。

#include "motor_control.h" #include "board_init.h" void main(void) { // 板级初始化:时钟、GPIO、ePWM、ADC、QEP Board_Init(); // 模型初始化 motor_control_initialize(); // 使能全局中断和实时中断 EINT; while (1) { // 主循环可以放通讯、状态指示等低优先级任务 // 控制算法在ADC中断中调用 step() } } // ADC中断服务函数 interrupt void adc_isr(void) { // 执行控制算法:采样值已由ADC模块转成模型输入变量 motor_control_step(); // 清除中断标志 PieCtrlRegs.PIEACK.all = 0xFFFF; }

3.2 把生成代码集成进CCS工程

拿到生成代码后,在CCS里新建一个工程,把模型生成的.c和.h文件全部加进去。这里有个实用建议:不要手动挑选文件,直接把生成输出文件夹整体加入工程,因为Simulink生成的文件之间可能有隐藏的依赖关系,少一个文件就是一堆莫名其妙的错误。

接下来要处理链接命令文件.cmd。F28335有对应的RAM和Flash链接配置,通常可以从芯片厂商的例程包或者别的工程里复制修改。如果只是调试阶段,可以先链接到RAM;量产阶段再做一次完整的Flash烧写配置。需要注意,Bootloader、SCI或向量表会占掉一部分RAM,链接脚本要留出足够的空间,否则局部变量稍微多一点,程序就可能跑飞。

初始化顺序也有讲究。Board_Init()必须在motor_control_initialize()之前执行,因为模型初始化时可能要读取一些外设状态,比如编码器清零、PWM同步信号初始化。如果初始化顺序反了,模型内部的一部分变量会基于错误的外部状态初始化,上电第一个控制周期就可能是异常输出。

3.3 拿PMSM电流环举例,整个流程实际走一遍

我用一个很典型的FOC电流环项目来说明,这个项目的控制对象是一台PMSM永磁同步电机。电流环运行频率10kHz,速度环1kHz,PWM载波也是10kHz。这个选择很常见,电流环和PWM频率一致,可以保证采样和调制同步,避免不必要的跳动。

在Simulink模型里,电流环算法从ADC采到的U相、V相电流开始,先做Clarke变换得到αβ轴电流,再做Park变换,结合编码器给出的电角度,得到旋转坐标系下的d轴和q轴电流。两个PI调节器分别控制d轴和q轴电流,输出Vd和Vq,再经过反Park变换得到Vα和Vβ,最后送进SVPWM模块生成三个相桥臂的占空比。

离线仿真阶段,我不用真实的电机模型,直接用一台理想电机的数学模型,或者用Simscape里搭建的PMSM本体。给定一个电流指令阶跃,观察电流环响应时间、超调量、稳态误差。这时所有物理量我都刻意用标幺值处理,比如电流标幺值基准设为50A,电压标幺值基准设为母线电压的某个比例。这样PI参数在仿真和实机上的含义是对应的,不会出现仿真调好了上板又得重调的局面。

上板之后再做一次开环测试,也就是指令给常数占空比,看三相电流是否平衡、编码器角度是否稳定。这一步确认硬件链路没问题后,再切换到闭环电流控制。我实测下来,生成的代码在F28335上跑10kHz电流环,CPU占用率很低,整个控制中断的执行时间大约只占CPU负载的20%到30%。这个性能给速度环、通讯、保护逻辑留下了充足余量。

3.4 Flash与RAM分配和优化

生成代码默认的内存分配不一定是最优的。Simulink里大量使用局部变量、缓冲区,如果在CCS工程里不做任何设置,链接器可能会把代码和数据都放在默认段里。对于一个常规的电机控制模型,256KB Flash通常够用,34KB RAM在大部分情况下也是够的,但一旦模型里出现了大尺寸查表数组(比如存储非线性磁链曲线、温度补偿表),就要注意RAM占用了。

遇到这种场景,我的做法是把常量查表数组直接放在Flash里,通过const关键字或者Simulink的存储类设置实现。这样既节省RAM,也避免上电初始化时从Flash复制一份到RAM的浪费。工程里经常有低速数据和实时数据混在一起的情况,我习惯把控制相关的变量放在RAM,把配置参数、校准表放在Flash,让链接脚本明确指定各自的段位置。

做完内存分配后,检查.map文件是个好习惯。重点关注.text段、.bss段、.stack段的size,如果stack的值异常小,程序复杂一点就极有可能栈溢出。栈溢出是最难查的bug之一,表现出来就是变量无端被改写、程序跑飞。27235这种老芯片,堆栈默认分配往往偏小,我一般会在链接脚本里手动调大一些。

4. 上板调试的经典问题和排查思路

4.1 仿真完美但上板就炸的三种常见病根

“明明Simulink里跑得好好的,怎么到了板子上就不是那个波形了?”这是我被问过最多的一句话。按照我的经验,绝大多数情况跑不出三种病根。

第一种是采样延时没有建模。控制器从触发ADC转换到真正拿到转换结果,存在一个固定的延时,大约是几微秒。Simulink的连续模型里,反馈是瞬时得到的,没有这个延时的概念。如果你在模型里没有加入这个延时,上板之后电流环的相位裕量变差,容易出现高频振荡。解决办法其实很简单:在反馈通路上加一个Z-1单位延时模块,模拟采样延时,然后重新整定PI参数。

第二种是PWM更新与算法不同步。如果你在某个中断里计算占空比,但这个中断和PWM载波的相位没有对齐,就会出现占空比跳变、电流波形毛糙。正确的做法是用ePWM的SOC事件触发ADC,ADC转换完成后再触发算法执行,这样采样、计算、更新三步都在同一个PWM周期内有序完成。

第三种是上电初始状态不一致。模型仿真时,所有积分器初始状态都是0,上板后电路里实际存在的电压电流并不为0。如果输出端没有做初始限幅,第一个控制周期就可能输出很大的占空比,直接把功率器件打炸。我建议在模型里对所有输出和积分器设置明确的初始条件,同时在硬件上留一组限幅,双保险。

4.2 PWM与ADC触发不同步导致的控制抖动

前面强调过好几次“同步”,因为这个问题真的很要命。我早期做的一个数字电源项目,电流采样用CPU定时器触发,结果纹波大得离谱,无论怎么调PI都没有改善。后来拿示波器观察,发现ADC采样点相对PWM开关沿在随机漂移,电流纹波在不同周期里采到的位置完全不一样,反馈值上下乱跳,控制器当然稳不住。

改用ePWM的SOCA信号触发ADC之后,问题立刻缓解了。原因是采样点被强制放在PWM周期内一个固定的相对位置,电流纹波在这个位置上的瞬时值基本一致,反馈数据变得干净平滑。这里有个更进一步的优化:可以配置SOC的相位偏移,让采样点落在电流纹波的平均值附近,而不是纹波峰值或者谷值。这样采样出来的电流更接近真实平均值,控制效果也更好。

如果你发现修改了触发方式后电流波形依然有周期性扰动,可以检查一下ADC转换时间是否跨越了PWM周期的边界。当PWM周期快到10kHz以上时,ADC转换时间相对更长,如果触发位置太靠近周期末尾,转换还没完成算法就已经被中断触发,采到的就是上一轮的值。这个可以通过调整SOC相位偏移让转换窗口避开PWM事件来解决。

4.3 代码生成后编译报错的经典处理

第一次把Simulink生成的代码嵌入CCS工程时,大概率会碰见几个固定报错。最常见的是“找不到头文件”,原因很简单——Simulink生成代码的头文件分布在多个目录里,特别是C2000支持包自带的外设头文件,不在CCS默认的include路径下。我的做法是把整个生成目录、Matlab安装目录下相关的C2000支持包路径、芯片头文件路径全部加进CCS的include设置里,一次性把路铺平。

第二个经典问题是中断函数符号冲突。Simulink如果配置了中断模块,生成代码里会自带一个中断服务函数,函数名往往比较固定。如果你在手写代码里也定义了一个同名函数,CCS直接报重复定义。规避办法是二选一:要么模型里的中断不生成ISR,只生成step函数,由你自己的ISR调用;要么所有中断服务都交给模型,手写代码里不要再定义同名ISR。我习惯用前一种,因为底层中断的现场保护和优先级管理还是手写代码更可控。

第三个坑是编译选项不匹配。生成代码对编译器标准、宏定义有要求,比如需要支持ISO C99,需要定义特定的处理器宏。如果CCS工程里用的优化等级和生成代码的假设不一致,可能出现一段代码被优化掉了、变量被变成不可见等诡异问题。我通常先用默认配置编译通过,稳定后再调整优化等级,并且每次只改一个变量做验证。

4.4 浮点运算、积分饱和与在线观测的细节

F28335的FPU对浮点运算是大杀器,但浮点运算也有自己的陷阱,最大的坑就是饱和和溢出。模型里PI调节器的输出如果不做限幅,误差持续累积时可能输出一个超出实际物理范围的占空比,SVPWM就会进入过调制甚至错误扇区,逆变器输出波形直接畸变。所以PI输出后面务必加Saturation或SaturationDynamic模块,而且限幅值要跟SVPWM最大可用电压对齐。

另一个跟浮点相关的问题是精度和表示范围。28335是单精度FPU,double和float在运算单元里其实都是按类似的方式处理,但在代码里混用会让编译器做额外转换。我建议在Simulink模型里统一使用double类型,生成代码里保持类型一致。如果某个信号要和寄存器交互,比如把占空比小数转换成PWM比较寄存器整数,必须显式Data Type Conversion,隐式类型转换在生成代码里容易产生与仿真不一致的结果。

在线观测也是一个很关键的实践环节。上板之后不能每次调一个变量就停下来点仿真器看,那样效率太低。我会用SCI或者UDP把关键变量周期性发到上位机,上位机用脚本解析并画曲线。这样一来调整PI参数时可以实时看到电流波形变化,整个调试过程跟用示波器看波形一样顺畅。CCS自带的Graph工具也能看变量,但灵活性稍微差一些。

5. 做完几个项目之后的一些体会

5.1 模型化开发真正改变的是什么

做过两三个28335的电机控制和数字电源项目之后,我最大的感受是:Simulink模型化开发真正改变的并不是“写代码”这个动作,而是把控制方案验证和嵌入式实现这两个环节打通了。以前手写C,算法对不对要等代码写完、板子调完才知道;现在模型阶段就能验证大部分问题,上板调试时遇到的问题类型完全不一样,更多是硬件时序、采样链路、EMI这些工程问题,而不是算法逻辑错误。

这种转变对团队协作的影响是实质性的。控制工程师可以聚焦在参数、算法和稳定性上,嵌入式工程师可以聚焦在驱动、中断和系统架构上,两者同时推进,最后在接口处汇合。我还是挺推荐刚入行的工程师尽早接触模型化开发思路,即使公司还没有完全推行MBD,学会了这个流程,对理解从控制理论到工程实现的完整链路也很有帮助。

5.2 给转型者的几条实际建议

第一,不要一上来就想把整个项目塞进Simulink。从一个电流环或者一个速度环开始,把它跑通,生成代码上板,看到真实电机转起来之后,再慢慢扩大范围。第二,模型里的固定步长、采样延时、数据类型的设置,从一开始就要按嵌入式工程的规范来做,别偷懒用默认值,否则后面迁移的时候全是隐形炸弹。第三,前期一定要花时间把PI的饱和限幅和积分清零策略做好,这个比算法里任何一个细节都更能决定系统是否稳定。

我个人踩过最深的坑,是把离线仿真的参数直接挪到板子上。后来才搞清楚,仿真里的电机模型是理想化的,实际平台的电流传感器偏置、逆变器管压降、PWM死区都会引入误差。现在我的做法是,先在模型里预留这些非理想因素的补偿通道,上板后再根据实测数据逐项标定。不要指望仿真参数一次到位,标定过程本身就是控制工程里躲不掉的核心工作。

最后再分享一个小经验:工具链的版本匹配能省下大量无谓的时间。Simulink、支持包、CCS、芯片头文件,这四个版本只要有一个明显偏旧,就可能出现兼容问题。每更换一次环境,我都会先跑一个最小模型做全链路验证,确认代码生成和烧录调试都正常,再开始正式开发,这个习惯帮我少折腾了无数次环境问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:45:22

LSTM-Adaboost组合模型在电力负荷预测中的Matlab实现

1. 项目破题:电力负荷预测为什么非要做成LSTM-Adaboost做电力负荷预测这个方向也有几年了,坦白说,这个领域最不缺的就是模型——从早期的ARIMA、指数平滑,到后来的SVM、随机森林,再到前几年火得不行的深度学习&#xf…

作者头像 李华
网站建设 2026/10/20 8:45:13

数据科学课程期末复习:从数据清洗到回归建模的完整知识体系

期末复习季又到了,一学期攒下来的PPT、练习题、小测卷子堆在桌面,很多人第一反应是“这么多内容怎么背得完”。但DATA2001这类数据课程,如果靠死记硬背去复习,基本会挂得很难看。它不是一门“背概念”的课,而是一门需要…

作者头像 李华
网站建设 2026/10/20 8:44:54

深度学习轨道异物检测系统毕设实战:从数据到部署

1. 毕设选题与需求拆解1.1 为什么“轨道异物检测”值得做成毕设这几年带过不少师弟师妹做毕业设计,有个很深的体会:选课题就像选工作方向,既要能撑起工作量,又要让答辩评委一看就觉得“这题目有价值、有难度、有实际意义”。“基于…

作者头像 李华
网站建设 2026/10/20 8:44:28

西门子S7-1200主从PLC通信编程实战:Modbus RTU从零到联调

两台S7-1200各管一个工艺段,中央控制柜还要把数据汇总;或者一台1200带了一堆远程设备,需要轮询采集模拟量、开关量,再下发执行信号——这种现场在中小型自动化项目里实在太常见了。"西门子1200主从PLC编程与应用指南"这…

作者头像 李华
网站建设 2026/10/20 8:44:21

Midscene.js:AI驱动的前端自动化测试,用自然语言告别脆弱选择器

很长一段时间里,一提到前端自动化测试,很多人的第一反应不是“放心”,而是“又要写一堆选择器了”。明明产品功能已经写完了,还得花半天时间对着DOM结构找class、定位xpath,结果产品经理一句“按钮文案改一下”&#x…

作者头像 李华
网站建设 2026/10/20 8:43:36

从GTF文件构建基因注释表,彻底解决ID转换难题

很长时间以来,做基因表达分析的人都绕不开一个重复又枯燥的步骤:拿到一个测序项目的结果,里面的基因ID是Ensembl ID,数据库注释是Symbol,文章里要用的却可能是另一个平台的ID。每次都要跑到各个注释数据库网页上批量查…

作者头像 李华