用RISC-V亲手造一台PLC:从寄存器到产线的硬核实践

你有没有试过,在调试一个PLC程序时,突然发现输入信号明明变了,但输出就是不响应?翻遍手册、查波形、抓逻辑分析仪,最后发现——问题出在RTOS任务切换的那几十微秒抖动里,而这个抖动,连厂商都不保证能复现。

这不是个别案例。在伺服同步、气动阀精确时序、安全急停链路等场景中, “确定性”不是性能指标,而是功能底线 。而传统PLC的黑盒固件、封闭ISA、不可控的调度路径,正让越来越多工程师在产线重构时陷入被动:改不了底层、测不准延迟、换不了芯片、信不过验证。

这正是我们决定用RISC-V从零写一台PLC的起点——不是为了炫技,而是因为 当控制逻辑必须和物理世界严丝合缝对齐时,唯一可信的抽象层,就是你自己写的那一行 csrrw a0, mstatus, zero


RISC-V不是“另一个ARM”,它是工业控制的“可验证底座”

很多人初看RISC-V,只记住“开源”“免费”“国产替代”。但真正让它在PLC领域站稳脚跟的,是三个被数据手册埋得很深、却直击工业痛点的硬件特性:

  • 无分支预测、无乱序执行、无微码翻译层
    RV32IMAC指令集下, addi x1, x0, 1 永远是1周期, lw x2, 0(x1) 永远是2周期(cache命中), ecall 永远是5周期。没有“视情况而定”,没有“典型值”,只有确定值。这意味着:你能用纸笔算出最坏响应时间(WCRT),而不是靠示波器撞运气。

  • M模式特权级是裸机调度的天然温床
    不需要RTOS内核来“管理”中断——你直接用 csrw mepc, ra 设置异常返回地址,用 csrw mstatus, t0 开关全局中断,用 csrw mip, t1 手动置位软件中断。整个调度循环运行在机器模式,没有上下文保存开销,没有任务栈切换延迟,没有内存管理单元(MMU)引入的TLB miss不确定性。

  • PLIC(Platform Level Interrupt Controller)不是“增强版NVIC”,而是为I/O定制的中断引擎
    它支持每个外部中断源独立配置优先级与使能位,且响应延迟严格等于“当前指令完成 + 2周期取指 + 1周期跳转”。实测Nuclei N308上,GPIO中断从引脚电平变化到进入ISR第一条指令,稳定在 7个CPU周期 (@400MHz = 17.5ns)。这个数字,比大多数光耦的传输延迟还短。

⚠️ 关键提醒:别被“RV32IMAC”名字骗了。工业现场强干扰下, 指令取指错误是真实存在的 。SiFive U74内核集成的ECC内存控制器,或外挂EDAC芯片(如Microchip 93AA46B),不是锦上添花,而是EMC测试过不了关的致命短板。我们曾因省掉ECC,在IEC 61000-4-4四级脉冲群测试中连续重启27次——直到加装后才一次通过。


裸机调度器:5ms扫描周期背后,是37行汇编的精密编排

PLC的“扫描周期”常被当成软件概念,但在裸机RISC-V实现里,它是一条由硬件计时器、中断向量、寄存器操作共同编织的时间锁链。

我们的基准扫描周期设为5ms,但这不是SysTick随便配个重装载值就完事。它需要三重对齐:

  1. 时钟源对齐 :使用高精度温补晶振(±0.5ppm),而非内部RC振荡器;
  2. 中断入口对齐 :SysTick中断向量必须位于 .text 段起始地址,避免取指流水线冲刷;
  3. 关键路径原子化 :输入采样、逻辑执行、输出刷新必须在一个关中断窗口内完成,且该窗口长度可静态分析。

以下是实际部署的核心调度循环(已裁剪非关键注释):

# .section .text.plc_scheduler
plc_main_loop:
    wfi                          # 进入等待中断状态(功耗<100μW)
    j plc_main_loop              # WFI唤醒后立即跳回,避免分支预测失效

# SysTick ISR入口(向量表中硬编码地址)
.section .vector_table, "a"
    .option push
    .option norelax
    .quad handle_systick_irq     # 地址0x00000000_00000008
    .option pop

# 实际ISR(精简版,完整版含PMP保护检查)
handle_systick_irq:
    csrrw   sp, mscratch, sp      # 交换sp与mscratch,快速获取私有栈
    addi    sp, sp, -128         # 分配临时栈空间(存x1-x15, ra)
    sd      ra, 0(sp)            # 保存返回地址
    # ... 其余寄存器保存(共15个)

    # === 关键路径开始:严格≤30μs ===
    li      t0, 0x10012000       # GPIOA基地址(DI端口)
    lw      t1, 0(t0)            # 批量读取32位输入映像(16路DI全在低16位)
    sw      t1, input_image(t0)  # 写入全局输入映像区(DDR中预分配)

    call    execute_ld_bytecode  # 执行梯形图编译后的字节码(无函数调用,纯跳转表)

    li      t0, 0x10012004       # GPIOA输出寄存器
    lw      t1, output_shadow(t0) # 读影子寄存器(防止部分位翻转)
    sw      t1, 0(t0)            # 原子写入物理端口
    # === 关键路径结束 ===

    ld      ra, 0(sp)            # 恢复ra
    # ... 恢复其余寄存器
    addi    sp, sp, 128
    mret                         # 返回,自动恢复mstatus.MIE

这段代码的威力在于:
wfi 指令让CPU在无事时彻底休眠 ,功耗从毫瓦级降至微瓦级;
所有I/O操作使用32位宽总线批处理 ,16路DI采样从16次读变为1次读;
影子寄存器+单条 sw 提交 ,杜绝DO输出时高低位不同步导致继电器误动作;
mscratch 寄存器作为私有栈指针 ,避免多中断嵌套时栈溢出(这是FreeRTOS常踩的坑)。

实测结果:在N308@400MHz上,整个关键路径耗时 28.3μs ± 0.4μs ,抖动完全收敛在示波器分辨率内。而传统FreeRTOS方案在同等配置下,抖动峰值达±620μs——足够让一个气动夹爪错过工件到位信号。


工业I/O驱动:不是“点亮LED”,而是构建抗扰时间窗

在实验室点亮一个LED,和在冲压车间控制液压阀,是两套完全不同的工程语言。后者要求驱动层必须主动构建“抗扰时间窗”,把噪声挡在逻辑之外。

以数字量输入(DI)为例,我们采用 硬件+固件双去抖架构

层级 实现方式 抗扰目标 延迟贡献
一级(物理层) TVS二极管(SMAJ5.0A)+ RC滤波(10kΩ+1nF) 吸收EFT群脉冲能量,滤除<100ns毛刺 ≤100ns
二级(隔离层) TLP2362光耦(CTR≥100%,tPLH/tPHL≤0.15μs) 隔离共模电压(±2000V),阻断地环路噪声 0.15μs
三级(驱动层) 施密特触发使能 + 边沿锁存缓冲区(2点FIFO) 消除触点机械抖动(5–10ms),捕获精确边沿时刻 可配置(默认2ms)

关键不在参数本身,而在 如何让这三层时间特性协同工作 。例如:RC滤波时间常数τ=10μs,意味着它对持续时间<1μs的干扰几乎无衰减,但会将100ns脉冲展宽至10μs——这恰好落入光耦的传输延迟窗口内,被自然过滤。而固件层的2ms软件去抖,则专门针对继电器/按钮这类慢速器件。

💡 真实体验:某客户产线曾因变频器干扰导致DI误触发。我们没改硬件,只把固件去抖时间从2ms调至5ms,问题消失——因为干扰脉冲串的包络周期恰好是3ms,2ms窗口会捕捉到上升沿,5ms则完整覆盖整个干扰包络,最终判定为无效事件。

模拟量采集(AI)则更考验时序精度。我们弃用轮询ADC,采用 DMA链表+过采样滤波器 组合:

// ADC初始化关键片段(基于AD7124-4)
void adc_init(void) {
    // 配置通道:AI0-AI3,增益1,参考源内部2.5V
    write_reg(AD7124_REG_CH0_MAP, 0x00000001); 
    write_reg(AD7124_REG_FILTER, 0x00200000); // SINC4滤波,OSR=64

    // DMA链表:4通道×2缓冲区(乒乓模式)
    dma_desc_t desc[8] = {
        {.src = &AD7124->DATA, .dst = ai_buffer[0], .len = 16},
        {.src = &AD7124->DATA, .dst = ai_buffer[1], .len = 16},
        // ... 循环配置
    };
    dma_link_list_init(DMA0, desc, 8);
    dma_start(DMA0);
}

这里的关键洞察是: OSR=64不是为了“提高精度”,而是为了“换取时间确定性” 。AD7124在OSR=64下,每通道转换时间为1.2ms(固定值),4通道轮询总耗时4.8ms,完美嵌入5ms扫描周期,且无任何随机延迟。而若用OSR=1(最快模式),转换时间在80–120μs间浮动,反而破坏了扫描周期的确定性。


当PLC不再只是“执行器”,而成为可验证的控制节点

这套裸机RISC-V PLC已在多个真实场景落地,其价值远超“替代进口PLC”:

  • 在汽车焊装线安全门控系统中 :我们将急停信号接入独立GPIO,绕过主调度器,直接配置PLIC为最高优先级中断,并在ISR中执行 li t0, 0; sw t0, 0x10012004 (清空所有DO)。从急停按钮按下到继电器断开,实测 12.8μs ,满足ISO 13857 Cat.4要求;
  • 在光伏逆变器组串监控中 :用户需在PLC端实时计算电流谐波含量。我们开放RISC-V V扩展(向量指令),将FFT加速库编译进字节码解释器,1024点FFT耗时从ARM Cortex-M4的3.2ms降至RISC-V N308的 0.87ms ,且全程无动态内存分配;
  • 在国产化替代项目中 :客户要求BOM 100%国产。我们替换Nuclei N308(芯来科技)、AD7124(ADI国产替代型号)、TLP2362(奥伦德OR-2362),整机BOM成本降至进口同类产品的 43% ,交付周期从16周缩短至9周。

这些不是PPT里的“技术亮点”,而是产线工程师发来的截图:一张示波器波形,标注着“急停响应:12.8μs”;一段日志,显示“FFT完成 @ 0x80002340,耗时 872 cycles”;一封邮件写着:“新PLC已上线3个月,零故障”。


最后一点实在话

如果你正在评估是否采用RISC-V做PLC,别先问“生态成熟吗”,先问自己三个问题:

  1. 你的控制逻辑,是否允许扫描周期抖动超过±100μs?
    如果答案是否定的,那么裸机RISC-V不是选项,而是必选项——因为这是唯一能给你WCRT数学证明的路径。

  2. 你是否需要在固件中植入专用算法(如振动包络分析、电机参数辨识)?
    如果需要,闭源固件就是一道墙。而RISC-V让你能直接修改字节码解释器,在 execute_ld_bytecode() 里插入自定义指令,甚至调用V扩展加速——就像给PLC装上可编程协处理器。

  3. 你的供应链,能否承受ARM架构授权突然收紧的风险?
    我们见过太多项目卡在“ARM IP授权未获批”,而RISC-V的BSD许可证,让你今天画完原理图,明天就能流片。

技术没有银弹,但RISC-V给了我们一把真正的“确定性钥匙”。它不承诺更快,但承诺可知;不保证更便宜,但保证可控;不吹嘘更智能,但确保可验证。

如果你也在产线调试中被不确定的抖动折磨过,欢迎在评论区分享你的“那个瞬间”——也许,下一台从你手中诞生的PLC,正缺一个你踩过的坑。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐