ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ARM7与μC/OS-II焊接机控制系统设计实战

ARM7与μC/OS-II焊接机控制系统设计实战 1. 项目缘起与整体设计思路1.1 为什么选ARM7加μC/OS-II这套组合做焊接机控制系统这个方向十几年我经手的方案从8位单片机裸机跑到工控机加运动控制卡最后落到ARM7加μC/OS-II这套组合上中间踩过的坑足够写一本小册子。先说结论ARM7加μC/OS-II不是性能最强的方案但在中低端焊接机控制这个细分场景里它是性价比、实时性和开发效率三者平衡得最好的选择之一。焊接机控制系统的核心诉求其实很朴素多路焊头要按精确时序动作温度要闭环控制送丝速度要跟焊接电流联动还要有人机界面和故障保护。这些任务天然带有并发性和实时性要求裸机跑前后台架构在任务少的时候还能凑合一旦任务超过五六个中断嵌套一深时序就开始飘。我见过太多用裸机写的焊接机焊点一致性差返修率高得离谱根子就在任务调度上。ARM7内核典型如LPC2000系列、AT91SAM7系列主频一般在40到60MHz带片内Flash和SRAM外设资源够用关键是价格便宜供货稳定。μC/OS-II是抢占式实时内核源码开放移植成熟任务调度确定性好最坏中断延迟可以算得出来。这两者搭配相当于用经济型轿车的预算买到了带独立悬挂的底盘跑烂路不至于散架。注意选ARM7不是因为它快而是因为它的中断响应和内存模型足够可预测。焊接机控制最怕的不是慢是“时快时慢”μC/OS-II的确定性调度恰好补上了这一环。1.2 系统整体架构怎么搭整个控制系统的架构我习惯分成四层硬件驱动层、内核与中间件层、控制逻辑层、人机交互层。硬件驱动层直接操作寄存器封装GPIO、PWM、ADC、UART、定时器等外设内核与中间件层跑μC/OS-II管理任务、信号量、消息队列、内存块控制逻辑层是焊接工艺的核心包括温度PID、送丝电机控制、焊头时序状态机人机交互层负责按键、显示、参数存储和报警。任务划分上我一般会切成这么几个任务名称优先级周期/触发方式职责安全监控任务最高5ms周期急停、过流、过温检测焊头时序任务高1ms周期多焊头动作时序控制温度控制任务中高10ms周期热电偶采样与PID运算送丝控制任务中20ms周期电机速度闭环人机交互任务中低50ms周期按键扫描、显示刷新参数存储任务低事件触发EEPROM读写这个划分的逻辑是越靠近安全和工艺核心的优先级越高、周期越短。安全监控必须最高优先级因为它要在任何异常发生时立刻切断输出晚几毫秒可能就是烧焊头甚至出安全事故。焊头时序任务周期定1ms是因为焊接动作的时序精度直接决定焊点质量1ms的分辨率对大多数电阻焊和弧焊够用了。1.3 任务间通信怎么设计μC/OS-II提供的通信机制里我用得最多的是消息队列加信号量的组合。温度控制任务算完PID输出后不直接去操作PWM寄存器而是把目标占空比通过消息队列发给执行任务焊头时序任务在关键节点用信号量同步送丝任务保证“先送丝到位再起弧”这个顺序不会乱。这里有个经验消息队列的长度要留足余量但也不能无限大。我一般按最坏情况下的消息产生速率乘以最大允许延迟来估算。比如温度任务10ms发一次执行任务最坏20ms处理一次那队列长度至少3实际我会设成8留出抖动空间。队列太短会丢消息太长则内存浪费且故障时消息积压难排查。提示μC/OS-II的消息队列是FIFO如果某个任务处理不过来消息会一直堆积。我习惯在调试阶段给每个队列加一个“水位监控”超过阈值就点灯报警上线前再关掉。2. 核心细节解析与实操要点2.1 μC/OS-II在ARM7上的移植关键点移植μC/OS-II到ARM7核心是三个文件OS_CPU.H、OS_CPU_C.C、OS_CPU_A.S。前两个定义数据类型和钩子函数第三个是汇编写的任务切换和中断处理。很多人移植失败问题八成出在中断向量表和堆栈切换上。ARM7有两种模式用户模式和特权模式。μC/OS-II的任务跑在用户模式内核跑在特权模式。任务切换时要通过软中断SWI从用户模式陷入特权模式保存现场、切换堆栈、恢复新任务现场再返回用户模式。这个过程里堆栈指针的切换必须原子操作中间不能被中断打断否则现场就乱了。我移植时的做法是在OSCtxSw里先关中断保存R0-R12、LR、PC到当前任务堆栈然后调用OSTaskSwHook再切换OSTCBCur指针最后恢复新任务的寄存器并开中断返回。OSIntCtxSw类似但用于中断服务程序末尾的切换区别是它不需要再保存现场因为中断入口已经保存过了。; OSCtxSw 核心片段ARM7模式 OSCtxSw STMFD SP!, {LR} ; 保存返回地址 STMFD SP!, {R0-R12, LR} ; 保存通用寄存器和LR MRS R0, SPSR ; 保存状态寄存器 STMFD SP!, {R0} LDR R0, OSTCBCur ; 获取当前任务控制块指针 LDR R1, [R0] STR SP, [R1] ; 保存当前任务堆栈指针 BL OSTaskSwHook ; 调用钩子函数 LDR R0, OSTCBCur LDR R1, OSTCBHighRdy LDR R2, [R1] STR R2, [R0] ; 切换当前任务控制块 LDR SP, [R2] ; 恢复新任务堆栈指针 LDMFD SP!, {R0} MSR SPSR_cxsf, R0 ; 恢复状态寄存器 LDMFD SP!, {R0-R12, LR} ; 恢复通用寄存器 LDMFD SP!, {PC}^ ; 返回并恢复CPSR这段代码里最容易出错的是最后一行LDMFD SP!, {PC}^那个^符号表示同时恢复CPSR少了它任务切换后中断使能状态就错了表现为系统跑着跑着就死机。2.2 中断管理焊接机控制的命门焊接机控制系统里中断用得非常密集PWM周期中断、ADC转换完成中断、定时器比较中断、串口接收中断、外部急停中断。μC/OS-II要求中断服务程序必须成对调用OSIntEnter()和OSIntExit()前者通知内核进入中断后者在退出时判断是否需要任务切换。我见过不少项目在这里翻车有人在中断里直接调用OSTimeDly()这是绝对禁止的因为延时函数会触发任务调度而中断上下文里不允许调度。正确的做法是在中断里只做最紧急的事比如置标志、发信号量把耗时操作丢给任务去做。急停中断的处理尤其讲究。急停信号来了中断服务程序要立刻关闭PWM输出这个动作必须在微秒级完成不能等任务调度。我的做法是在急停中断里直接操作PWM控制寄存器把输出强制拉低然后再发一个信号量给安全监控任务让任务去处理报警显示和状态记录。注意中断服务程序的执行时间要尽可能短。我一般要求每个ISR不超过50微秒超过这个数就要考虑拆分。焊接机里ADC采样中断如果处理太慢会影响下一个采样周期的启动导致温度采样失真。2.3 温度PID控制的参数整定焊接温度控制用PID是标配但参数整定是个手艺活。我的经验是先调P再调I最后看情况加D。焊接机的热惯性大纯P控制会有稳态误差加上I能消除误差但I太强会振荡。D项在温度控制里作用有限因为热电偶信号本身有噪声微分会把噪声放大。具体操作上我会先把I和D置零逐渐增大P直到温度开始等幅振荡记下此时的临界增益Ku和振荡周期Tu然后用Ziegler-Nichols经验公式算出初始参数P0.6KuI2TuDTu/8。这组参数作为起点再根据实际焊接效果微调。实际调试时我会在焊头上贴一个校准过的热电偶和控制系统用的热电偶做对比确保采样通道没有系统性偏差。这个步骤很多人省结果PID调得再好实际温度还是偏因为输入本身就是错的。参数作用调大效果调小效果焊接机典型值Kp比例增益响应快易振荡响应慢稳态误差大8~15Ki积分增益消除误差快易超调消除误差慢0.5~2Kd微分增益抑制超调放大噪声超调增大0~0.52.4 焊头时序控制的实现细节多焊头焊接机的时序控制是核心难点。假设一台机器有4个焊头每个焊头要依次完成“下压、通电、保持、断电、抬起”五个动作每个动作的时间参数可调还要保证焊头之间不互相干扰。我用的是状态机加定时器链表的方案。每个焊头维护一个独立的状态机状态转移由定时器到期事件驱动。所有焊头的定时器挂在一个有序链表上链表按到期时间排序定时器中断里只需要检查链表头到期就触发对应焊头的状态转移然后重新插入链表。这样中断处理时间是O(1)跟焊头数量无关。typedef struct { uint8_t state; // 当前状态 uint16_t duration; // 当前状态持续时间 uint16_t elapsed; // 已过时间 uint8_t next_state; // 下一状态 } WeldingHead_t; // 定时器中断里调用 void TimerTick(void) { WeldingHead_t *head GetEarliestHead(); if (head head-elapsed head-duration) { head-state head-next_state; head-elapsed 0; head-duration GetDuration(head-state); ReinsertToTimerList(head); } }这个方案的好处是扩展性好从4个焊头加到8个代码几乎不用改只是链表长一点。坏处是链表操作要小心插入和删除时如果中断来了指针可能被改乱。我的做法是在链表操作前后关中断虽然会引入几微秒的延迟但保证了数据一致性。3. 实操过程与核心环节实现3.1 开发环境搭建与工程配置我用的工具链是Keil MDK加J-Link调试器这套组合在ARM7开发里最顺手。新建工程后第一步是配置启动文件把堆栈大小设够。μC/OS-II每个任务都要独立堆栈任务越多、局部变量越大需要的RAM就越多。我一般给每个任务至少256字节堆栈安全监控任务给512字节因为它要保存更多现场信息。工程目录结构我习惯这样组织Project/ ├── CMSIS/ # 内核相关头文件 ├── Drivers/ # 外设驱动 │ ├── gpio.c │ ├── pwm.c │ ├── adc.c │ └── uart.c ├── OS/ # μC/OS-II源码与移植文件 │ ├── Source/ │ └── Ports/ ├── App/ # 应用层 │ ├── main.c │ ├── task_safety.c │ ├── task_weld.c │ ├── task_temp.c │ └── task_hmi.c └── Config/ # 配置文件 ├── os_cfg.h └── app_cfg.hos_cfg.h里要仔细配置把不用的功能关掉省空间。比如事件标志组如果不用就设OS_FLAG_EN为0内存管理如果只用固定大小块就设OS_MEM_EN为1但OS_MEM_SIZE调小。我见过有人把能开的全开了结果Flash不够用又回头一个个关浪费时间。3.2 任务创建与优先级分配实操任务创建在main()里完成先调用OSInit()初始化内核再逐个OSTaskCreate()最后OSStart()启动调度。优先级分配有个原则周期越短、实时性要求越高的任务优先级数字越小优先级越高。μC/OS-II里优先级0最高我一般把0留给安全监控1给焊头时序2给温度控制依次往下。int main(void) { OSInit(); OSTaskCreate(TaskSafety, NULL, TaskSafetyStk[STK_SIZE-1], 0); OSTaskCreate(TaskWeld, NULL, TaskWeldStk[STK_SIZE-1], 1); OSTaskCreate(TaskTemp, NULL, TaskTempStk[STK_SIZE-1], 2); OSTaskCreate(TaskWire, NULL, TaskWireStk[STK_SIZE-1], 3); OSTaskCreate(TaskHMI, NULL, TaskHMIStk[STK_SIZE-1], 4); OSTaskCreate(TaskStorage, NULL, TaskStorageStk[STK_SIZE-1], 5); OSStart(); return 0; }每个任务的主体是个死循环循环里必须有阻塞点否则低优先级任务永远得不到执行。阻塞点可以是OSTimeDly()、OSSemPend()、OSQPend()等。我见过新手写的任务里没有阻塞结果高优先级任务一直跑低优先级任务饿死系统看起来像死机。3.3 温度采样与滤波处理热电偶信号很微弱一般是毫伏级要先经过仪表放大器放大到0到3.3V范围再送ARM7的ADC。ADC采样值有噪声直接送PID会震荡所以要先滤波。我用的是滑动平均加中值滤波的组合先连续采5个点去掉最大最小剩下3个取平均。这样既能抑制脉冲噪声又能平滑随机噪声。#define SAMPLE_NUM 5 uint16_t FilterTemp(uint16_t raw) { static uint16_t buf[SAMPLE_NUM] {0}; static uint8_t idx 0; uint16_t temp[SAMPLE_NUM]; uint8_t i, j; buf[idx] raw; idx (idx 1) % SAMPLE_NUM; for (i 0; i SAMPLE_NUM; i) { temp[i] buf[i]; } // 冒泡排序 for (i 0; i SAMPLE_NUM - 1; i) { for (j 0; j SAMPLE_NUM - 1 - i; j) { if (temp[j] temp[j1]) { uint16_t t temp[j]; temp[j] temp[j1]; temp[j1] t; } } } // 去掉最大最小取中间三个平均 return (temp[1] temp[2] temp[3]) / 3; }这个滤波函数每10ms调用一次计算量很小在ARM7上跑几十微秒就完事。滤波后的值再查表转换成实际温度查表法比公式计算快精度也够。3.4 送丝电机速度闭环控制送丝速度直接影响焊缝质量速度不稳会导致焊缝宽窄不一。我用的是直流电机加编码器反馈PWM驱动速度环用PI控制。编码器每转输出固定脉冲数定时器对脉冲计数每20ms读一次计数值算出实际转速和设定值比较PI调节器输出新的PWM占空比。这里有个细节PWM频率要选得合适。太低电机会有啸叫太高驱动管开关损耗大。我一般选20kHz左右刚好在听觉范围上限附近电机运行安静驱动管用普通MOSFET也能扛住。void TaskWire(void *pdata) { float set_speed 0.0f; float actual_speed 0.0f; float error 0.0f; float integral 0.0f; float kp 2.5f, ki 0.8f; float output 0.0f; while (1) { actual_speed GetEncoderSpeed(); error set_speed - actual_speed; integral error * 0.02f; // 20ms周期 // 积分限幅防止饱和 if (integral 100.0f) integral 100.0f; if (integral -100.0f) integral -100.0f; output kp * error ki * integral; // 输出限幅 if (output 100.0f) output 100.0f; if (output 0.0f) output 0.0f; SetPWM(output); OSTimeDly(2); // 20ms } }积分限幅很重要没有它电机堵转时积分项会累积到很大一旦堵转解除电机会猛冲一下送丝量突变焊缝直接报废。4. 常见问题与排查技巧实录4.1 系统跑飞与死机排查ARM7加μC/OS-II的系统跑飞九成以上是堆栈溢出。每个任务的堆栈大小是静态分配的如果任务里用了大数组或者递归调用堆栈就会溢出踩到相邻任务的数据表现就是随机死机。排查方法是在任务切换钩子里检查堆栈指针是否越界或者用OSTaskStkChk()函数定期检查每个任务的堆栈使用情况。我一般会在调试阶段把每个任务的堆栈填充成0xAA运行一段时间后看有多少0xAA被改写就知道实际用了多少堆栈。这个方法简单有效比猜靠谱得多。另一个常见原因是中断优先级配置错误。ARM7的VIC向量中断控制器可以配置每个中断的优先级如果两个中断优先级相同且同时触发行为就不确定。我的原则是急停最高PWM周期中断次之ADC再次串口最低。串口中断里不要做复杂处理收到数据丢进缓冲区就退出。故障现象可能原因排查方法解决措施随机死机任务堆栈溢出填充0xAA检查增大堆栈或减少局部变量时序漂移中断延迟过大示波器测中断响应时间优化ISR关中断时间缩短温度振荡PID参数不当观察温度曲线重新整定PID通信丢包串口中断被屏蔽检查中断使能状态调整中断优先级焊头误动作信号量使用错误检查信号量配对确保Pend和Post成对4.2 中断延迟优化的几个狠招焊接机控制对中断延迟很敏感我总结了几条优化经验。第一关中断的时间要尽可能短临界区里只放必须原子执行的代码比如链表指针更新其他能挪出去的都挪出去。第二中断服务程序用汇编写关键部分C编译器生成的代码有时候会多几条指令在微秒级竞争里这几条指令就是胜负手。第三把不紧急的中断降级为任务比如串口接收中断里只把数据存到环形缓冲区解析协议的事交给任务做。实测下来优化前中断延迟大概15微秒优化后能压到5微秒以内。别小看这10微秒在高速焊接场景里10微秒的时序误差可能导致焊点强度差一个等级。4.3 参数存储与掉电保护焊接参数要掉电保存我用的是片内EEPROM或者外挂SPI Flash。写EEPROM有个坑写操作期间如果掉电数据可能写一半。我的做法是双备份加校验和写的时候先写备份区校验通过后再写主区读的时候如果主区校验失败就切到备份区。这样即使写一半掉电至少还有一个完整的数据。typedef struct { uint16_t magic; // 魔数标识数据有效 uint16_t params[32]; // 焊接参数 uint16_t checksum; // 校验和 } ParamBlock_t; uint8_t SaveParams(ParamBlock_t *p) { p-magic 0x55AA; p-checksum CalcChecksum(p); // 先写备份区 EEPROM_Write(BACKUP_ADDR, (uint8_t*)p, sizeof(ParamBlock_t)); if (VerifyBlock(BACKUP_ADDR) ! OK) return ERR; // 再写主区 EEPROM_Write(MAIN_ADDR, (uint8_t*)p, sizeof(ParamBlock_t)); if (VerifyBlock(MAIN_ADDR) ! OK) return ERR; return OK; }这个双备份机制我用了很多年救过不少现场。有一次客户现场电网不稳频繁掉电别的厂家的机器参数丢了好几次我们的机器一次没丢客户后来追加了订单。4.4 电磁干扰与硬件滤波焊接机工作时有高频逆变电磁干扰很强ARM7系统容易被干扰到复位或者ADC采样跳变。硬件上电源入口要加共模电感和TVS管信号线用屏蔽线屏蔽层单端接地。PCB布局上晶振尽量靠近芯片底下不要走线模拟地和数字地分开最后单点连接。软件上ADC采样加数字滤波前面说的滑动平均加中值滤波就是干这个的。另外关键寄存器定期刷新比如PWM控制寄存器如果被干扰改写了输出就乱了。我在安全监控任务里每5ms重写一次PWM配置寄存器确保输出状态始终正确。提示电磁干扰导致的故障往往很难复现排查时可以用示波器抓电源纹波和信号波形如果看到尖峰毛刺基本就是干扰问题。先解决硬件软件滤波是辅助不能本末倒置。5. 性能优化与扩展思考5.1 任务调度开销的实测与优化μC/OS-II的任务切换开销主要在保存和恢复寄存器上ARM7有37个寄存器切换一次大概需要几十条指令。我实测过在60MHz主频下一次任务切换大约3到5微秒。如果任务切换太频繁比如每1ms切换好几次CPU时间就都花在切换上了。优化方法是合并任务。比如按键扫描和显示刷新本来可以分成两个任务但它们的实时性要求都不高合并成一个50ms周期的任务完全够用。任务少了切换次数就少CPU利用率就上去了。我一般把任务数量控制在6到8个超过10个就要仔细评估每个任务的必要性。另一个优化点是减少信号量和消息队列的使用。每次Pend和Post都有开销如果两个任务之间数据传递不频繁可以用全局变量加关中断保护代替。全局变量访问快但要注意原子性读写多字节变量时要关中断。5.2 从ARM7升级到Cortex-M的考量ARM7是经典内核但毕竟老了现在新项目我基本都转到Cortex-M3/M4。Cortex-M有硬件除法、位带操作、更低的功耗和更高的性价比。μC/OS-II也有Cortex-M的移植版本代码改动量不大主要是启动文件和汇编部分要重写。如果现有ARM7项目要升级我的建议是分步走先把应用层代码和硬件驱动层解耦驱动层用统一的接口封装这样换平台时只需要重写驱动层应用层几乎不动。然后在新平台上跑通μC/OS-II再逐个移植任务每移植一个就测试一个不要一次性全搬过去。升级的收益是明显的Cortex-M4带FPUPID运算用浮点更快主频可以到100MHz以上任务周期可以缩得更短功耗更低适合电池供电的便携焊接设备。成本上Cortex-M芯片现在比ARM7还便宜供货也更好。5.3 焊接工艺参数的智能化扩展现在的焊接机控制系统参数都是人工设定老师傅凭经验调。下一步可以往智能化方向走用历史焊接数据训练模型自动推荐参数。比如根据板材厚度、材质、焊丝直径自动算出最佳的电流、电压、送丝速度和焊接时间。这个扩展不需要改硬件在现有ARM7系统上加个通信接口把数据传到上位机或者云端做训练训练好的模型再下发到设备上。ARM7的运算能力跑复杂模型不够但跑简单的查表或者线性回归没问题。如果要用更复杂的模型就得升级到Cortex-M4或者带NPU的芯片。我在一个项目里试过用查表加线性插值的方式做参数推荐效果还不错。把常见工况的参数做成表格存在Flash里根据输入条件查表表格点之间线性插值。这个方法简单可靠不需要联网适合工业现场。5.4 系统可靠性设计的经验总结工业设备最看重可靠性我的经验是冗余加自检。冗余方面关键传感器用双路比如温度采样用两个独立的热电偶通道读数偏差超过阈值就报警。自检方面上电时做一次全面自检包括RAM读写测试、Flash校验、外设回路测试运行中定期做关键寄存器回读。看门狗是必须的但看门狗喂狗策略要讲究。我不用定时器中断喂狗因为如果主循环卡死但中断还在跑看门狗就失效了。我的做法是在最低优先级任务里喂狗这个任务能跑到说明所有高优先级任务都没阻塞系统是健康的。如果最低优先级任务跑不到看门狗超时复位系统重启。最后分享一个我踩过的坑有一次现场反馈机器偶尔重启查了半天没找到原因最后发现是看门狗喂狗任务优先级设太高了它自己能跑但低优先级任务饿死系统看起来正常实际上已经失控。把喂狗任务降到最低优先级后问题解决。这个教训告诉我看门狗喂狗任务的优先级必须是最低的这是铁律。
返回列表