3、指令集设计:PLC常用指令(LD、AND、OR、OUT、TIM、CNT)的二进制编码与操作码设计
指令集设计,说白了就是给PLC芯片定一套“暗号”。
CPU不认识“LD X0”这种助记符,它只认0和1。所以,我们必须把每条指令翻译成二进制码。这个翻译过程,就是操作码(Opcode)设计。
我个人习惯把指令集设计比作“字典”。你查字典,先找到拼音(助记符),然后看到解释(操作码)。今天我们就来编这本字典。
3.1 指令格式:定长还是变长?
先定个规矩:一条指令占几个字节?
PLC逻辑扫描引擎追求的是确定性。我建议用定长指令,每条指令固定为4字节(32位)。
为什么?
你想想看,扫描引擎要逐条取指、译码、执行。如果指令长度不固定,取指逻辑会变得复杂,容易产生时序瓶颈。我在项目中遇到过,变长指令虽然节省存储空间,但让流水线设计变得很痛苦。
所以,我们定下来:每条指令32位。
| 位域 | 宽度 | 说明 |
|---|---|---|
| 操作码 | 8位 | 指令类型,如LD、AND、OR等 |
| 操作数类型 | 4位 | 指定操作数是X、Y、M、T、C等 |
| 操作数地址 | 16位 | 具体地址编号,如X0、Y10等 |
| 预留/标志 | 4位 | 用于扩展或特殊标志 |
嗯,这里要注意:操作码8位,理论上可以支持256条指令。对于PLC常用指令来说,绰绰有余。
3.2 操作码分配:给每条指令一个“身份证号”
操作码就是指令的ID。我习惯用十六进制表示,清晰明了。
| 助记符 | 操作码(十六进制) | 功能描述 |
|---|---|---|
| LD | 0x01 | 取常开触点,将触点状态载入累加器 |
| LDI | 0x02 | 取常闭触点,取反后载入 |
| AND | 0x03 | 与运算,串联常开触点 |
| ANI | 0x04 | 与非运算,串联常闭触点 |
| OR | 0x05 | 或运算,并联常开触点 |
| ORI | 0x06 | 或非运算,并联常闭触点 |
| OUT | 0x07 | 输出结果到指定线圈 |
| TIM | 0x08 | 定时器指令,设定延时时间 |
| CNT | 0x09 | 计数器指令,设定计数值 |
| END | 0xFF | 程序结束,扫描周期复位 |
核心原则:操作码分配要留有余地。0x0A到0xFE都是空闲的,方便以后扩展。我曾经见过一个项目,操作码用得太满,后来想加个“上升沿检测”指令,只能硬挤,搞得兼容性很差。
3.3 指令编码示例:从助记符到二进制
光说理论不过瘾,我们直接看例子。
例1:LD X0
- 操作码:0x01
- 操作数类型:X寄存器,编码为0x1
- 操作数地址:X0,地址为0x0000
- 完整32位:0x01100000(二进制:0000 0001 0001 0000 0000 0000 0000 0000)
例2:OUT Y10
- 操作码:0x07
- 操作数类型:Y寄存器,编码为0x2
- 操作数地址:Y10,地址为0x000A
- 完整32位:0x0720000A
例3:TIM T0 K100
- 操作码:0x08
- 操作数类型:T寄存器,编码为0x4
- 操作数地址:T0,地址为0x0000
- 定时值:K100,编码为0x0064(100的十六进制)
- 完整32位:0x08400064
小技巧:对于TIM和CNT这类需要立即数的指令,我把操作数地址的低16位用来存放定时值/计数值。这样不用额外增加指令长度。嗯,这是空间换时间的做法。
3.4 译码逻辑:CPU怎么读懂这些二进制?
指令编码好了,CPU怎么执行?
说白了,就是查表。CPU内部有一个译码器,它根据操作码,跳转到对应的处理逻辑。
我习惯用C语言风格描述译码逻辑,方便大家理解:
// 伪代码:指令译码执行
void execute_instruction(uint32_t instr) {
uint8_t opcode = (instr >> 24) & 0xFF;
uint8_t operand_type = (instr >> 20) & 0x0F;
uint16_t operand_addr = instr & 0xFFFF;
switch(opcode) {
case 0x01: // LD
acc = read_operand(operand_type, operand_addr);
break;
case 0x03: // AND
acc = acc & read_operand(operand_type, operand_addr);
break;
case 0x05: // OR
acc = acc | read_operand(operand_type, operand_addr);
break;
case 0x07: // OUT
write_operand(operand_type, operand_addr, acc);
break;
case 0x08: // TIM
start_timer(operand_addr, operand_addr); // 地址域存定时值
break;
case 0x09: // CNT
start_counter(operand_addr, operand_addr);
break;
case 0xFF: // END
reset_scan();
break;
default:
// 非法指令,触发错误
error_flag = 1;
break;
}
}
你看,译码逻辑其实不复杂。关键是把操作码和功能一一对应好。
避坑指南:我曾经犯过一个错误——把操作码0x00也分配出去了。后来发现,0x00在硬件复位后是默认值,导致未初始化的指令被误执行。所以,操作码0x00一定要保留为NOP(空操作)或非法指令。
3.5 指令集设计的核心流程图
下面这张图,概括了从助记符到二进制编码,再到CPU译码执行的完整流程。
3.6 指令集设计的几个关键点
最后,我总结几个实战中容易踩的坑:
- 操作码不要从0x00开始——原因前面说了,硬件复位默认值问题。
- 同类指令的操作码尽量连续——比如LD、LDI用0x01、0x02,AND、ANI用0x03、0x04。这样译码时可以用位运算快速判断指令类别。
- 预留扩展空间——PLC指令集不是一成不变的。我建议至少留50%的操作码空间给未来扩展。
- 操作数类型编码要统一——X、Y、M、T、C的编码一旦定下来,就不要改了。改一次,所有固件和工具链都要跟着改,很痛苦。
一句话总结:指令集设计是PLC芯片的“宪法”。定好了,后面所有工作都顺;定不好,后面全是坑。我建议你花时间把操作码表反复推敲几遍,再动手写代码。
好了,这一章的内容就到这里。指令编码搞定了,下一步就是把这些指令串起来,形成真正的扫描执行逻辑。
公众号:蓝海资料掘金营,微信deep3321