1. 为什么值得把野火PID调试助手协议搬进自己的项目
搞电机控制的朋友大概率都经历过这个场景:辛辛苦苦把电流环、速度环、位置环都调通了,波形也能跑起来,但一到整定PID参数就抓瞎。改一次参数、重新编译、烧录、观察波形,一轮下来五分钟没了,一天下来调不出三组像样的参数。更别提三环级联的时候,内环外环互相耦合,靠盲调基本等于买彩票。
野火出的那个PID调试助手,本质上解决的就是这个痛点。它把下位机的实时数据通过串口上传到上位机,上位机画出曲线,你在电脑上拖动滑块就能改Kp、Ki、Kd,下位机立刻生效,波形实时刷新。这套东西如果自己从零写,通信协议、数据打包、波形渲染、参数下发,没个三五天搞不定。但如果你直接把它的通信协议移植到自己的STM32工程里,半天就能用上。
这篇文章面向的是已经在做电机控制、手里有STM32工程、想快速接入上位机调试工具的开发者。不管你是用霍尔编码器做速度闭环,还是用位置式PID做位置控制,或者是级联PID做三环调速,这套协议都能直接用。我会把协议格式、移植步骤、代码实现、踩坑经验全部拆开讲清楚,你照着抄作业就行。
先说清楚一个前提:野火PID调试助手支持两种模式,一种是简单模式(只发数据),一种是高级模式(支持参数读写)。我们移植的是高级模式协议,因为只有高级模式才能实现"上位机改参数、下位机实时生效"这个核心功能。简单模式只能看波形,调参还得重新烧录,意义不大。
2. 协议格式拆解与通信帧设计逻辑
2.1 帧结构全貌与各字段作用
野火PID调试助手的通信协议是一个典型的自定义串口协议,帧结构不算复杂,但每个字段都有明确用途。完整的一帧数据由帧头、命令字、数据长度、数据载荷、校验和、帧尾组成。
| 字段 | 字节数 | 说明 |
|---|---|---|
| 帧头 | 2 | 固定为0xAA 0x55 |
| 命令字 | 1 | 区分数据方向和数据类型 |
| 数据长度 | 1 | 载荷字节数 |
| 数据载荷 | N | 实际传输的数据 |
| 校验和 | 1 | 从帧头到载荷的累加和取低8位 |
| 帧尾 | 1 | 固定为0x5A |
帧头用0xAA 0x55是有讲究的。0xAA的二进制是10101010,0x55是01010101,两者交替出现,在示波器上非常容易识别,也方便接收方做字节对齐。很多自定义协议喜欢用单个0xFF做帧头,但0xFF在数据中出现的概率太高,容易误判。双字节帧头加上固定帧尾,基本可以保证帧同步的可靠性。
命令字决定了这一帧是干什么的。上位机发给下位机的命令包括:读取参数、写入参数、启动数据上传、停止数据上传。下位机发给上位机的命令包括:参数应答、数据上传。每个命令字对应一个固定的数值,移植的时候需要和上位机保持一致。
数据长度字段告诉接收方后面有多少个有效数据字节。这个字段的存在让协议变得灵活,不同命令可以携带不同长度的数据,接收方不需要为每种命令单独写解析逻辑。
校验和用的是最简单的累加和取低8位。虽然CRC校验更可靠,但累加和计算量小,在STM32上几乎不占时间,而且对于短帧数据来说,累加和的检错能力已经够用了。实际测试下来,只要串口线不太长、波特率不太高,累加和基本不会漏检。
2.2 数据上传帧的格式与浮点数传输
下位机上传的数据帧是最常用的。每一帧可以携带多个通道的数据,每个通道用4个字节的float表示。为什么用float而不是int?因为PID的输出量、反馈量、设定值都可能是小数,用int会丢失精度。比如速度环的反馈值可能是123.45 RPM,用int就只能传123,调参的时候看着不舒服。
float在内存中是4字节,但传输的时候需要按字节拆开。这里有个坑:STM32是小端模式,float的低字节在低地址。如果你直接用一个union把float和uint8_t数组叠在一起,发出去的字节顺序就是小端。上位机如果也按小端解析就没问题,但如果上位机按大端解析,数据就全乱了。野火的上位机是按小端解析的,所以STM32这边直接发就行,不需要做字节序转换。
数据上传帧的载荷结构是这样的:第一个字节是通道数量,后面依次是每个通道的4字节float数据。比如你要上传3个通道(设定值、反馈值、输出值),载荷就是1+3×4=13字节。上位机收到后,按顺序解析出3个float,分别画三条曲线。
这里有个细节需要注意:通道数量是固定的还是可变的?野火的上位机支持配置通道数量,但配置信息是在上位机端设置的,下位机只管按固定格式发。所以你在下位机代码里要写死通道数量,或者通过一个宏定义来配置。我一般习惯上传3个通道:目标值、实际值、PID输出值。这三个量足够看出控制效果了。
2.3 参数读写帧的格式与交互流程
参数读写是高级模式的核心。上位机要改Kp,就发一帧写参数命令,载荷里包含参数编号和新的参数值。下位机收到后,更新对应的PID参数,然后回一帧应答,告诉上位机"我收到了"。
参数编号是一个字节,范围0到255。你可以自己定义编号规则,比如0x01代表速度环Kp,0x02代表速度环Ki,0x03代表速度环Kd,0x11代表位置环Kp,以此类推。上位机那边也要配置同样的编号映射,否则改的参数对不上。
参数值用4字节float传输,和上传数据一样。写参数帧的载荷是1+4=5字节:第一个字节是参数编号,后面4字节是参数值。读参数帧的载荷只有1字节,就是参数编号。下位机收到读参数请求后,回一帧应答,载荷是1+4=5字节:参数编号+当前参数值。
交互流程是这样的:上位机发写参数帧→下位机解析→更新参数→回写参数应答帧→上位机收到应答→更新界面显示。如果下位机没有回应答,上位机会认为通信失败,可能会弹窗报错。所以应答帧一定要发,哪怕参数更新失败也要回一个带错误标志的应答。
注意:参数编号的映射关系必须在上位机和下位机之间严格一致。我见过有人下位机用0x01表示Kp,上位机配置成0x01表示Ki,结果调了半天发现调的是错的参数。建议在代码里用宏定义把编号和参数名绑定,比如#define PARAM_SPEED_KP 0x01,这样不容易搞混。
3. STM32端协议移植的完整实操
3.1 串口外设配置与DMA接收
移植的第一步是把串口配好。野火PID调试助手默认波特率是115200,8位数据位,1位停止位,无校验。STM32的USART配置成一样的参数就行。如果你用的是HAL库,直接调HAL_UART_Init,把波特率设成115200,字长设成8位,停止位设成1位,校验位设成None。
但光配好串口还不够,接收数据的方式很关键。如果你用中断接收,每来一个字节进一次中断,115200波特率下每秒最多11520个字节,中断频率太高,会占用大量CPU时间。电机控制本身就要跑PID计算、PWM更新、编码器读取,CPU资源本来就紧张,再被串口中断频繁打断,控制周期会抖动。
我推荐用DMA接收。DMA可以在不占用CPU的情况下把串口数据搬到内存缓冲区,等一帧数据收完了再通知CPU处理。具体做法是:开一个足够大的接收缓冲区(比如256字节),配置DMA为循环模式,串口每收到一个字节就自动存到缓冲区里。然后开一个空闲中断(IDLE),当串口总线空闲超过一个字节时间时,触发空闲中断,在中断里计算收到了多少字节,然后置一个标志位,主循环里处理。
空闲中断加DMA的组合是STM32串口接收的经典方案,实测非常稳。唯一需要注意的是DMA缓冲区的索引计算。在空闲中断里,用缓冲区总大小减去DMA剩余传输数量,就是本次收到的字节数。处理完之后要重置DMA计数器,否则下次接收会从上次的位置继续。
// 串口DMA接收初始化示例(HAL库) #define RX_BUFFER_SIZE 256 uint8_t rxBuffer[RX_BUFFER_SIZE]; uint16_t rxLen = 0; volatile uint8_t rxFlag = 0; void UART_DMA_Init(void) { // 启动DMA接收 HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE); // 使能空闲中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); } // 空闲中断处理 void USART1_IRQHandler(void) { if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 计算接收长度 rxLen = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); rxFlag = 1; // 停止DMA,准备重新配置 HAL_UART_DMAStop(&huart1); // 重新启动DMA接收 HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE); } HAL_UART_IRQHandler(&huart1); }上面这段代码是核心框架。实际用的时候,rxFlag置位后要在主循环里尽快处理,处理完了把rxFlag清零。如果处理太慢,下一帧数据来了会覆盖缓冲区,导致丢帧。不过对于PID调试来说,丢一两帧数据问题不大,上位机画曲线的时候会自动插值。
3.2 协议解析状态机的实现
收到一帧数据后,需要解析出命令字和载荷。最直接的方法是写一个解析函数,先找帧头0xAA 0x55,然后读命令字、数据长度、载荷,最后校验。但这种方法有个问题:如果数据中恰好出现了0xAA 0x55,会被误认为是帧头。虽然概率不高,但一旦发生就会解析错位。
更稳妥的做法是用状态机。状态机逐个字节扫描,只有按顺序经过"帧头1→帧头2→命令字→长度→载荷→校验→帧尾"所有状态,才算一帧有效数据。如果中间任何一个字节不符合预期,就回到初始状态重新找帧头。这样即使数据中出现了0xAA 0x55,只要后续字节不满足状态转移条件,就不会被误判。
// 协议解析状态机 typedef enum { STATE_HEAD1, // 等待帧头1 STATE_HEAD2, // 等待帧头2 STATE_CMD, // 等待命令字 STATE_LEN, // 等待长度 STATE_DATA, // 接收载荷 STATE_CHECK, // 等待校验和 STATE_TAIL // 等待帧尾 } ParseState; ParseState state = STATE_HEAD1; uint8_t dataBuf[64]; uint8_t dataIndex = 0; uint8_t dataLen = 0; uint8_t checkSum = 0; uint8_t cmd = 0; void ParseByte(uint8_t byte) { switch(state) { case STATE_HEAD1: if(byte == 0xAA) state = STATE_HEAD2; break; case STATE_HEAD2: if(byte == 0x55) state = STATE_CMD; else state = STATE_HEAD1; break; case STATE_CMD: cmd = byte; checkSum = byte; state = STATE_LEN; break; case STATE_LEN: dataLen = byte; checkSum += byte; dataIndex = 0; if(dataLen > 0) state = STATE_DATA; else state = STATE_CHECK; break; case STATE_DATA: dataBuf[dataIndex++] = byte; checkSum += byte; if(dataIndex >= dataLen) state = STATE_CHECK; break; case STATE_CHECK: if(byte == (checkSum & 0xFF)) state = STATE_TAIL; else state = STATE_HEAD1; break; case STATE_TAIL: if(byte == 0x5A) { // 一帧完整数据,处理它 HandleFrame(cmd, dataBuf, dataLen); } state = STATE_HEAD1; break; } }这个状态机的好处是鲁棒性强,即使串口上混入了噪声数据,也能自动恢复到正常解析状态。而且状态机是逐字节处理的,不需要额外的缓冲区来存整帧数据,内存占用小。
3.3 参数映射表的设计与PID参数在线更新
参数映射表是连接上位机参数编号和实际PID变量的桥梁。最简单的做法是用一个结构体数组,每个元素包含参数编号、参数指针、参数类型。收到写参数命令后,遍历数组找到匹配的编号,然后通过指针更新对应的变量。
typedef struct { uint8_t id; // 参数编号 float *pValue; // 参数指针 float minVal; // 最小值 float maxVal; // 最大值 } ParamMap; // PID参数变量 float speedKp = 1.0f, speedKi = 0.1f, speedKd = 0.0f; float posKp = 2.0f, posKi = 0.0f, posKd = 0.05f; ParamMap paramTable[] = { {0x01, &speedKp, 0.0f, 100.0f}, {0x02, &speedKi, 0.0f, 10.0f}, {0x03, &speedKd, 0.0f, 10.0f}, {0x11, &posKp, 0.0f, 100.0f}, {0x12, &posKi, 0.0f, 10.0f}, {0x13, &posKd, 0.0f, 10.0f}, }; uint8_t UpdateParam(uint8_t id, float value) { for(int i = 0; i < sizeof(paramTable)/sizeof(ParamMap); i++) { if(paramTable[i].id == id) { // 限幅保护 if(value < paramTable[i].minVal) value = paramTable[i].minVal; if(value > paramTable[i].maxVal) value = paramTable[i].maxVal; *paramTable[i].pValue = value; return 1; // 成功 } } return 0; // 失败,未找到参数 }限幅保护很重要。上位机拖动滑块的时候,可能会不小心拖到很大的值,比如Kp拖到1000。如果没有限幅,电机可能瞬间疯转,轻则触发过流保护,重则烧驱动。加上限幅之后,即使上位机发了离谱的值,下位机也会自动截断到安全范围内。
实操心得:参数映射表里的minVal和maxVal要根据实际电机和负载来定。我一般会把Kp的上限设成理论计算值的2倍左右,Ki的上限设成Kp的1/10,Kd的上限设成Kp的1/5。这样即使上位机乱拖,也不会出大问题。
4. 数据上传与波形观测的实战细节
4.1 上传数据的时机与频率控制
数据上传的时机很关键。如果你在PID计算的中断里直接发串口数据,会阻塞中断,影响控制周期。正确的做法是在中断里把需要上传的数据存到一个缓冲区,然后在主循环里发送。这样中断只负责存数据,发送的事情交给主循环,互不干扰。
上传频率也要控制。野火上位机默认的刷新率是50Hz左右,也就是每20ms上传一帧。如果你上传太快,比如每1ms发一帧,串口带宽不够(115200波特率下,一帧13字节的数据加上帧头帧尾大概20字节,1ms发一帧就是20000字节/秒,超过了115200/10=11520字节/秒的上限),会导致数据堆积和丢帧。上传太慢又看不出波形细节。
我的做法是用一个定时器,每10ms触发一次数据上传。10ms对应100Hz的刷新率,对于大多数电机控制来说足够看出动态响应了。如果控制周期是1ms,那就是每10个控制周期上传一次数据。在控制中断里用一个计数器,计数到10就把数据存入上传缓冲区,主循环检测到缓冲区有数据就发送。
// 控制中断里的数据采集 volatile float uploadBuf[3]; volatile uint8_t uploadReady = 0; uint8_t uploadCounter = 0; void Control_ISR(void) // 1ms周期 { // PID计算... float target = ...; float actual = ...; float output = ...; uploadCounter++; if(uploadCounter >= 10) // 每10ms上传一次 { uploadCounter = 0; uploadBuf[0] = target; uploadBuf[1] = actual; uploadBuf[2] = output; uploadReady = 1; } } // 主循环里发送 void Main_Loop(void) { if(uploadReady) { uploadReady = 0; SendDataFrame(uploadBuf, 3); } // 其他任务... }4.2 浮点数打包与字节序处理
发送float数据的时候,需要把float拆成4个字节。最简洁的方法是用union:
typedef union { float f; uint8_t bytes[4]; } FloatUnion; void SendDataFrame(volatile float *data, uint8_t channelCount) { uint8_t frame[64]; uint8_t index = 0; uint8_t checksum = 0; // 帧头 frame[index++] = 0xAA; frame[index++] = 0x55; // 命令字(数据上传) frame[index++] = 0x01; checksum = 0x01; // 数据长度 uint8_t payloadLen = 1 + channelCount * 4; frame[index++] = payloadLen; checksum += payloadLen; // 通道数量 frame[index++] = channelCount; checksum += channelCount; // 各通道数据 for(int i = 0; i < channelCount; i++) { FloatUnion fu; fu.f = data[i]; for(int j = 0; j < 4; j++) { frame[index++] = fu.bytes[j]; checksum += fu.bytes[j]; } } // 校验和 frame[index++] = checksum & 0xFF; // 帧尾 frame[index++] = 0x5A; // 发送 HAL_UART_Transmit_DMA(&huart1, frame, index); }这里用HAL_UART_Transmit_DMA而不是阻塞发送,是为了避免发送过程阻塞主循环。DMA发送把数据交给DMA控制器后,CPU就可以去干别的事了,等发送完成中断来了再处理下一帧。
注意:如果上一帧还没发完,下一帧就来了,DMA会覆盖上一帧的数据。所以发送之前要检查DMA是否空闲。可以用一个标志位记录DMA发送状态,发送完成中断里清零标志位,发送前检查标志位。
4.3 上位机端的配置与联调
下位机代码写好后,上位机端也要配置。打开野火PID调试助手,选择串口和波特率,然后配置通道数量、参数编号映射、数据格式。通道数量要和下位机一致,参数编号映射也要和下位机的paramTable一致。
联调的时候,先不接电机,用手转动编码器或者用信号发生器模拟反馈信号,观察上位机能不能收到数据、波形对不对。确认数据通路没问题后,再接电机。接电机后先给很小的Kp,观察电机能不能稳定运行,然后逐步加大Kp直到出现等幅振荡,再减小到振荡消失,记录此时的Kp为临界增益。然后根据临界增益法(Ziegler-Nichols)计算Ki和Kd的初始值,再在上位机上微调。
5. 移植过程中最容易踩的坑与排查方法
5.1 通信类问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上位机收不到数据 | 串口线接反 | 交换TX和RX |
| 上位机收不到数据 | 波特率不匹配 | 检查两边波特率是否都是115200 |
| 数据波形乱跳 | 字节序不对 | 检查float打包顺序 |
| 数据波形乱跳 | 通道数量配置错误 | 检查上位机通道数是否和下位机一致 |
| 参数改了没反应 | 参数编号映射错误 | 检查paramTable和上位机配置 |
| 参数改了没反应 | 应答帧没发 | 检查下位机是否回了应答 |
| 偶尔丢帧 | 上传频率太高 | 降低上传频率或提高波特率 |
| 偶尔丢帧 | DMA缓冲区太小 | 增大RX_BUFFER_SIZE |
5.2 参数在线更新的安全性设计
参数在线更新最大的风险是:上位机发了错误的参数,导致电机失控。除了前面说的限幅保护,还有几个安全措施可以做。
第一个是参数更新使能开关。在代码里加一个全局变量paramUpdateEnable,只有这个变量为1的时候才允许更新参数。默认设为0,需要调参的时候通过一个特定的命令字打开。这样即使上位机误发了写参数命令,也不会生效。
第二个是参数变化率限制。如果新参数和旧参数的差值超过一定阈值,就拒绝更新。比如Kp从1突然跳到100,差值99超过了阈值50,就拒绝。这样可以防止滑块拖太快导致的参数突变。
第三个是看门狗。如果参数更新后电机失控,控制循环里的看门狗会检测到异常(比如电流过大、转速过高),自动复位或者切换到安全参数。这个需要结合具体的电机控制逻辑来实现。
// 参数更新安全保护 uint8_t paramUpdateEnable = 0; float lastSpeedKp = 1.0f; uint8_t SafeUpdateParam(uint8_t id, float value) { if(!paramUpdateEnable) return 0; // 变化率限制 if(id == 0x01) // speedKp { if(fabsf(value - lastSpeedKp) > 50.0f) return 0; lastSpeedKp = value; } return UpdateParam(id, value); }5.3 中断优先级与实时性保障
串口接收中断的优先级要设置合理。如果串口中断优先级太高,会打断PID控制中断,导致控制周期抖动。如果太低,又可能丢数据。我的建议是:PID控制中断优先级设为最高(比如抢占优先级1),串口空闲中断优先级设为中等(抢占优先级2),DMA发送完成中断优先级设为最低(抢占优先级3)。
这样PID控制永远能按时执行,串口接收稍微延迟一点也没关系,因为DMA已经在后台把数据存到缓冲区了,空闲中断晚一点处理只是晚一点解析,不会丢数据。
实操心得:我遇到过一个问题,串口空闲中断和PID中断优先级设成一样,结果串口数据量大的时候PID周期从1ms抖到了1.5ms,电机出现轻微异响。后来把PID中断优先级调高就好了。电机控制里,控制周期的稳定性比什么都重要。
6. 从单环到三环级联的协议扩展思路
6.1 多环参数的编号规划
单环调试跑通后,大概率要上三环级联。三环级联意味着有3组PID参数,每组3个参数,一共9个参数。如果再加上前馈系数、滤波系数,可能超过15个参数。参数编号规划要提前做好,不然后面加参数的时候容易乱。
我的编号方案是这样的:高4位表示环编号,低4位表示参数类型。0x1X表示电流环,0x2X表示速度环,0x3X表示位置环。每个环内,0xX1表示Kp,0xX2表示Ki,0xX3表示Kd,0xX4表示前馈,0xX5表示输出限幅。这样一眼就能看出参数属于哪个环、是什么类型。
| 编号 | 含义 | 编号 | 含义 |
|---|---|---|---|
| 0x11 | 电流环Kp | 0x21 | 速度环Kp |
| 0x12 | 电流环Ki | 0x22 | 速度环Ki |
| 0x13 | 电流环Kd | 0x23 | 速度环Kd |
| 0x14 | 电流环前馈 | 0x24 | 速度环前馈 |
| 0x15 | 电流环限幅 | 0x25 | 速度环限幅 |
| 0x31 | 位置环Kp | 0x32 | 位置环Ki |
| 0x33 | 位置环Kd | 0x34 | 位置环前馈 |
| 0x35 | 位置环限幅 |
6.2 多通道数据上传的通道分配
三环级联的时候,需要观测的数据也变多了。电流环要看目标电流和实际电流,速度环要看目标速度和实际速度,位置环要看目标位置和实际位置。如果全部上传,就是6个通道。野火上位机支持最多8个通道,6个通道完全没问题。
但通道多了之后,每帧数据变长,上传频率就要降低。6个通道的数据帧载荷是1+6×4=25字节,加上帧头帧尾和校验,一帧大概30字节。115200波特率下,一帧的传输时间是30×10/115200≈2.6ms。如果每10ms发一帧,占用2.6ms的串口时间,还有7.4ms的空闲,完全够用。
通道分配建议:通道1到通道3给最外环(位置环或速度环),通道4到通道6给内环(电流环)。这样上位机画曲线的时候,外环的曲线在上面,内环的曲线在下面,看起来层次分明。
6.3 级联PID调试的顺序与技巧
级联PID的调试顺序必须是从内到外。先调电流环,电流环稳定了再调速度环,速度环稳定了再调位置环。如果顺序反了,外环调好了内环一改,外环又乱了。
调电流环的时候,把速度环和位置环的输出限幅设成0,让它们不输出,只让电流环工作。给电流环一个阶跃给定,观察实际电流的响应。如果超调太大就减小Kp,如果响应太慢就增大Kp。电流环的带宽一般设成速度环的5到10倍,速度环的带宽设成位置环的5到10倍。
调速度环的时候,电流环保持已经调好的参数不变,给速度环一个阶跃给定,观察实际速度的响应。速度环的Kp决定了响应速度,Ki决定了稳态误差消除速度,Kd决定了抗扰动能力。速度环调好后,位置环的调试就简单了,因为位置环的输出就是速度环的给定,只要位置环的带宽远低于速度环,整个系统就是稳定的。
实操心得:级联PID调试的时候,我习惯先把所有积分项和微分项设成0,只调比例项。比例项调好了再加积分项,积分项调好了再加微分项。这样每一步只调一个参数,变量少,容易定位问题。如果三个参数一起调,出了问题都不知道是哪个参数引起的。
7. 移植后的性能评估与优化方向
7.1 通信延迟与数据实时性评估
移植完成后,我实测了一下通信延迟。从下位机采集数据到上位机显示波形,总延迟大概在30到50ms之间。这个延迟主要来自三个部分:下位机采集到发送的延迟(最多10ms,取决于上传周期)、串口传输延迟(约2.6ms)、上位机解析和渲染延迟(约20到40ms)。
对于PID调试来说,30到50ms的延迟完全可以接受。因为PID调试看的是趋势和稳态,不是看毫秒级的瞬态响应。如果你要看电流环的阶跃响应,那可能需要更低的延迟,这时候可以把上传周期缩短到2ms,但通道数量要减少到2个,否则串口带宽不够。
7.2 CPU占用率与内存开销
协议移植带来的CPU开销很小。DMA接收和发送几乎不占CPU,空闲中断每帧触发一次,解析状态机每字节执行一次switch,加起来大概几十个时钟周期。参数更新只在收到写参数命令时执行,频率很低。整体算下来,协议处理占用的CPU时间不到1%。
内存开销主要是接收缓冲区和发送缓冲区。接收缓冲区256字节,发送缓冲区64字节,参数映射表大概100字节,总共不到500字节。对于STM32F103C8T6这种64KB Flash、20KB RAM的芯片来说,完全无压力。
7.3 后续可扩展的功能方向
这套协议移植好之后,还可以扩展一些实用功能。比如数据记录功能:在上位机端加一个"开始记录"按钮,把接收到的数据存成CSV文件,方便后续用MATLAB或者Python做分析。再比如参数自整定功能:在上位机端实现继电器反馈法或者临界比例度法,自动计算PID参数并下发。
还有一个很实用的扩展是"参数组保存与加载"。调好一组参数后,把参数保存到Flash里,下次上电自动加载。这样就不用每次上电都重新调参了。STM32的Flash读写很简单,用HAL_FLASH_Program就行,注意擦除和写入的地址对齐。
最后再分享一个小技巧:如果你用的是Keil MDK开发环境,可以在Debug模式下用Logic Analyzer功能实时看变量波形,和野火上位机的波形对照着看。Logic Analyzer看的是芯片内部的变量,没有通信延迟,可以用来验证上位机波形的准确性。两者结合,调参效率翻倍。