1# @Language: Markdown
2# @Software: VS Code/MacDown/Typora/Vim
3# @Author  : Di Wang
4# @Email   : [email protected]

缘由: 最近看到了Gabriel创建的项目

感觉是应用费曼学习法的良机. 于是决定创建此文, 记录相关内容.

正如Gabriel所写:

This repository aims to collect examples of EPICS Databases to help newcomers (or not so new) get an idea on how to use different records.

即便已使用epics多年, 对一些record type依然不熟悉, 或源于需求少或源于版本更新带来的功能变更. 往往导致有简单方法却使用了更复杂的实现方式.

此文分为三部分, 一部分是epics database的示例, 另一部分是常用epics module的示例, 还有一些其它内容的示例. 一些epics module (如busy), 会创建了新的record type, 依然归入module. 而一些复杂的module, 如asyn, stream device则难以简单完成示例, 因此会另起专文记述.

由于内容较多, 会在闲暇时陆续更新, 此文大概会花费数年才能完成.


Module

autosave


caputlog

用法

log可以有JSON格式, 需要epics 7.0.1以上.

server可以使用iocLogServer.

1<IOC app name>_DBD += caPutLog.dbd  # For standard format
2<IOC app name>_DBD += caPutJsonLog.dbd  # For JSON format (Exists only if module is compiled with supported version of base)
3<IOC app name>_LIBS += caPutLog  # Required for both output formats

需要首先配置Access security, 对于DEFAULTgroup, 添加RULE(1,WRITE,TRAPWRITE). 然后在iocsh中, 设置

 1caPutLogInit "host[:port]" [config]
 2caPutJsonLogInit "host[:port]" [config]
 3# for example
 4caPutLogInit "localhost:7011" 0
 5caPutJsonLogInit $(CAPUTLOG_INET):$(CAPUTLOG_INET_PORT=8002) $(CAPUTLOG_OPTION=0)
 6
 7# 重新加载配置
 8caPutLogReconf config
 9caPutJsonLogReconf config
10
11# info
12caPutLogShow level
13caPutJsonLogShow level
14
15# 修改日期格式
16caPutLogSetTimeFmt "<date_time_format>"
17
18# log to a PV
19epicsEnvSet(EPICS_AS_PUT_LOG_PV, "pv.VAL$")
20epicsEnvSet(EPICS_AS_PUT_JSON_LOG_PV, "pv.VAL$")
21
22# debug
23var caPutLogDebug,1

示例

首先启动iocLogServer. 启动命令如下.

 1INSTALL_BIN=/home/sdcswd/epics/R7.0.8/base/bin/linux-x86_64
 2
 3# To change the default values for the EPICS Environment parameters,
 4# uncomment and modify the relevant lines below.
 5
 6# EPICS_IOC_LOG_PORT="6500" export EPICS_IOC_LOG_PORT
 7EPICS_IOC_LOG_FILE_NAME="ioclog.log" export EPICS_IOC_LOG_FILE_NAME
 8# EPICS_IOC_LOG_FILE_LIMIT="1000000" export EPICS_IOC_LOG_FILE_LIMIT
 9
10if [ $1 = "start" ]; then
11    if [ -x $INSTALL_BIN/iocLogServer ]; then
12        echo "Starting EPICS Log Server "
13        $INSTALL_BIN/iocLogServer &
14    fi
15else
16    if [ $1 = "stop" ]; then
17        pid=`ps -e | sed -ne '/iocLogSe/s/^ *\([1-9][0-9]*\).*$/\1/p'`
18        if [ "${pid}" != "" ]; then
19            echo "Stopping EPICS Log Server "
20            kill ${pid}
21        fi
22    fi
23fi

然后在st.cmd中

 1dbLoadTemplate("db/user.substitutions")
 2
 3epicsEnvSet(EPICS_IOC_LOG_INET, "localhost")
 4#epicsEnvSet(EPICS_IOC_LOG_PORT, "$(LOG_INET_PORT)")
 5epicsEnvSet(iocLogDisable, "$(LOGDISABLE=0)")
 6
 7asSetFilename("access.acf")
 8iocLogPrefix("${IOC} ")
 9
10cd "${TOP}/iocBoot/${IOC}"
11iocInit
12
13caPutLogInit "localhost:7004" 0
14iocLogInit()

然后在ioclog.log文件中, 可以同时得到iocLog(前两行)和caPutLog(后两行)的内容.

1localhost:55142 Sun May 12 15:26:47 2024 iocexample iocPause: IOC suspended
2localhost:55142 Sun May 12 15:26:47 2024 iocexample iocRun: IOC restarted
3localhost:55128 Sun May 12 15:26:57 2024 iocexample 12-May-24 15:26:51 zephyrus sdcswd sdcswd:circle:step new=2 old=1
4localhost:55128 Sun May 12 15:27:02 2024 iocexample 12-May-24 15:26:57 zephyrus sdcswd sdcswd:circle:step new=1 old=2

iocstats


recsync

用于Channel Finder. 也就是把ioc中的record信息, epics environment以及info tag发送到一个数据库.

但目前整个服务显得很繁杂, 要先开启Elasticsearch, 然后开启Channel Finder Service, 然后开启recsync中的server, 也叫RecCeiver, (这个名字很糟糕, 因为它其实是一个中间服务, 负责接受ioc发过来的内容, 然后发给Channel Finder Service), 最后开启带有recsync中的client功能(RecCaster)的ioc. client使用5049UDP端口.

server会调用pyCFClient module, 创建Python class ChannelFinderClient, 与Channel Finder Service通信, 通信时使用了默认的8080端口.

可以使用https://github.com/ChannelFinder/RecSync-env来简化部署.

以下为测试recsync的流程, 不涉及Channel Finder Service.

RecCaster

在ioc中添加reccaster依赖,

1test_DBD += reccaster.dbd
2test_LIBS += reccaster

在st.cmd中, 可以配置RecCaster

 1var(reccastTimeout, 5.0)
 2var(reccastMaxHoldoff, 5.0)
 3epicsEnvSet("CONTACT", "mycontact")
 4epicsEnvSet("BUILDING", "mybuilding")
 5epicsEnvSet("SECTOR", "mysector")
 6
 7addReccasterEnvVars("CONTACT", "SECTOR")
 8addReccasterEnvVars("BUILDING")
 9
10iocInit

RecCeiver

开启RecCeiver的流程

 1$ cd recsync/server
 2$ python3 -m venv venv
 3$ source venv/bin/activate
 4$ pip3 install --upgrade pip
 5$ pip3 install requests
 6$ pip3 install .
 7$ pip3 list
 8Package            Version
 9------------------ --------
10attrs              23.2.0
11Automat            22.10.0
12certifi            2024.2.2
13channelfinder      3.0.0
14charset-normalizer 3.3.2
15constantly         23.10.4
16hyperlink          21.0.0
17idna               3.7
18incremental        22.10.0
19pip                24.0
20recceiver          1.5
21requests           2.31.0
22setuptools         66.1.1
23simplejson         3.19.2
24six                1.16.0
25Twisted            24.3.0
26typing_extensions  4.11.0
27urllib3            2.2.1
28zope.interface     6.3
29# 或者直接指定PYTHONPATH
30$ export PYTHONPATH=${PWD}:${PWD}/build/lib:${channelfinder_path}
31$ twistd -r poll -n recceiver -f demo.conf

calc


busy


sequencer

Others


Access Security


record patch

可以把record定义在一个db file中, 然后在另一个db file中添加这个record的一些field (比如逻辑处理相关field). 但是要注意, dbLoadRecords的先后顺序.

也可以不写record的type, 用*来代替.

最近在GitHub issue上看到有人在提议添加功能来反向patch(即删除某些record), 估计不久后会加入主线.


field modifiers

历史包袱又一例, 具体因果暂时未能厘清, 但大致源于以下一些限制:

  • epics record name 长度限制 40 -> 60
  • epicsString 大小限制为 MAX_STRING_SIZEP (40)
  • CA 使用的DBR_STRING大小限制为MAX_STRING_SIZEP (40)

而record name又会被使用在db link中, (INP, DOL, OUT, FWD), 所以就出现了一个问题, 当某个link field使用了大于40字节的record name时, 无法通过caget caput来读写. 于是引入了$作为field modifier. 当在link field后面加上$后, 就不会使用string, 而是使用char数组.

 1$ caput -S long_string.INP$  SomeReallyLongRecordNameThatExceeds40Characters
 2Old : long_string.INP$ SomeReallyLongRecordNameThatExceeds40Ch NPP NMS
 3New : long_string.INP$ SomeReallyLongRecordNameThatExceeds40Characters NPP NMS
 4
 5$ caput -S long_string.INP  SomeReallyLongRecordNameThatExceeds40Characters
 6Old : long_string.INP                SomeReallyLongRecordNameThatExceeds40Ch
 7Error from put operation: Invalid element count requested
 8
 9$ caget -S long_string.INP$
10long_string.INP$ SomeReallyLongRecordNameThatExceeds40Characters NPP NMS
11
12$ caget -S long_string.INP
13long_string.INP                SomeReallyLongRecordNameThatExceeds40Ch

类似的, lsilso record的VAL field也可以使用$来修改CA的行为.



breakpoint table

TODO


Echoless IOC Comments

启动ioc时, 可以在st.cmd文件中使用#-来不让注释行输出. 非常有用. 示例

 1$ cat st.cmd
 2# print this comment
 3#- not print this comment
 4dbLoadRecords a.db
 5
 6$ softIocPVA st.cmd
 7# print this comment
 8dbLoadRecords a.db
 9iocInit
10Starting iocInit
11############################################################################
12## EPICS R7.0.8
13## Rev. 2024-03-27T10:23+0100
14## Rev. Date build date/time:
15############################################################################
16iocRun: All initialization complete
17epics>
18iocInit

Database (record type)

dbCommon

fieldinformation
NAME60 characters
DESC40 characters
ASG29 characters
SCANmenuScan
PINImenuPini
PHAS不建议使用
EVNTEvent scan类型
DTYPDevice Type
TSETime Stamp Event, 0, -1, -2, [1-255]
TSELTime Stamp Link
TIMEepicsTimeStamp time
DISSDisable Alarm Sevrty, menuAlarmSevr
DISVDisable Value, 只是不process, value依然可以改变, 与DISA比较, 相当的话disable
DISADisable, DBF_SHORT
SDISScanning Disable, INLINK, 读取后放入DISA
MLOKMonitor lock, epicsMutexId
MLISMonitor List, ELLLIST
BKLNKBackwards link tracking, ELLLIST
DISPDisable putField, 非0时不可通过CA或PVA修改field(除DISP)
PROCForce Processing, 写入时process record, 写0也可
STATAlarm Status, menuAlarmStat
SEVRAlarm Severity, menuAlarmSevr
AMSGAlarm Message
NSTANew Alarm Status, 用于内部处理过程, 处理完成后将最严重的级别更新到STAT
NSEVNew Alarm Severity
NAMSGNew Alarm Message
ACKSAlarm Ack Severity, highest unacknowledged alarm severity
ACKTAlarm Ack Transient, Alarm Ack Transient, menuYesNo
LCNTLock Count, 记录PACTTRUE时的数量, 超过10, 那就触发SCAN_ALARM
PACTRecord active, 为TRUE时代表record正被process, 类型是DBF_UCHAR而非menuYesNo
PUTFdbPutField process, 不详
RPROReprocess, 不详
ASPAccess Security Pvt, struct asgMember
PPNpprocessNotify, putNotify callback, struct processNotify
PPNRpprocessNotifyRecord, next record for PutNotify, struct processNotifyRecord
SPVTScan Private, struct scan_element
RSETstruct typed_rset
DSETunambiguous_dset
DPVTDevice Private, void *dpvt
RDESAddress of dbRecordType, struct dbRecordType
LSETLock Set, struct lockRecord
PRIOScheduling Priority, menuPriority
TPROTrace Processing, 非0时打印trace信息
BKPTBreak Point, 调试用断点, epicsUInt8 bkpt
UDFUndefined
UDFSUndefined Alarm Severity, menuAlarmSevr
UTAGTime Tag
FLNKForward Process Link

对于TSETSEL:

  1. TSEL指向另一个record的TIME field时, 使用获取到的TIME作为本record的timestamp. 如果是同一个ioc, UTAG也被复用.
  2. TSEL指向另一个record其它field时, 那个field的值被放到TSE中.
    1. TSE为0, 会从current time providers中找到优先级最高的, 获取timestamp.
    2. TSE为-1, 会从event time providers中找到优先级最高的, 然后由这个provider来提供event=-1的timestamp. 需要调用generalTimeEventTpRegister来注册event provider.
    3. TSE为-2, 从device support中获取timestamp和user tag, 由device support来决定读取time provider还是event provider还是不注册provider直接读硬件.
    4. TSE为[1, 255], 从event providers中找到优先级最高的, 然后由这个 provider提供对应event发生时的timestamp.

相关函数

 1// base中的一些macro
 2#define generalTimeCurrentTpName generalTimeCurrentProviderName
 3#define generalTimeEventTpName generalTimeEventProviderName
 4
 5#define epicsTimeEventCurrentTime 0
 6#define epicsTimeEventBestTime -1
 7#define epicsTimeEventDeviceTime -2
 8
 9int epicsShareAPI epicsTimeGetEvent(epicsTimeStamp *pDest, int eventNumber)
10{
11    if (eventNumber == epicsTimeEventCurrentTime) {
12        // TSE为0的情况
13        return epicsTimeGetCurrent(pDest);
14    } else {
15        // TSE为-1或[1-255]的情况
16        return generalTimeGetEventPriority(pDest, eventNumber, NULL);
17    }
18}
19
20// mrfioc2 处理longout record中TSE=-2的情况
21static long process_longout(longoutRecord *prec)
22{
23    priv *p=static_cast<priv*>(prec->dpvt);
24    long ret=0;
25
26    if (prec->val>=0 && prec->val<=255)
27        post_event(prec->val);
28
29    if(prec->tse==epicsTimeEventDeviceTime){
30        p->evr->getTimeStamp(&prec->time,p->event);
31    }
32}

对于disable record, 使用Access security可以禁止record被读写, 也可以用DISVSDIS禁止record被process.

 1record(ai, ai){
 2  field(DISV, "1")
 3  field(DISS, "MAJOR")
 4  field(VAL,  "311")
 5  field(SDIS, "bi PP")
 6}
 7
 8record(bi, bi){
 9  field(INP, "1")
10  field(PINI, "YES")
11}

Mostly Common

fieldinformation
RVAL对于input, 从hardware中读取的原始值, 待转换; 对于output, 转换后发送给hardware的值.
OVALOutput Value
RBVReadback Value
DOLDesired Output Link
OMSLsupervisory or closed_loop
ORAWPrevious Raw Value
PVALPrevious value
IVOAINVALID output action, 三个选项Continue normally, Don't drive outputsSet output to IVOV
IVOVINVALID output value
NORDNumber elements read, 用于waveform和aai

Simulation

开启simulation mode时, 会忽略device support. 不知道有什么实际用处.

fieldinformation
SIOLSimulation Input/Output Link
SVALSimulation Value
SIMLSimulation Mode Link
SIMMSimulation Mode
SIMSSimulation Mode Severity
OLDSIMMPrev. Simulation Mode
SSCNSim. Mode Scan
SDLYSim. Mode Async Delay
SIMPVTSim. Mode Private, epicsCallback
 1           --  (SIMM = NO?)
 2         /         (if supported and directed by device support,
 3        /              INP -> RVAL -- convert -> VAL),
 4                   (else INP -> VAL)
 5 SIML -> SIMM
 6
 7         \
 8           --  (SIMM = YES?) SIOL -> SVAL -> VAL
 9           \
10             -- (SIMM = RAW?) SIOL -> SVAL -> RVAL -- convert -> VAL

Analog Input Record (ai)

Unit conversion

当device support为Raw Soft Channel时才会转换.

fieldinformation
RVALCurrent Raw Value
ROFFRaw Offset
ASLOAdjustment Slope
AOFFAdjustment Offset
LINRLinearization MENU, menuConvert
ESLORaw to EGU Slope
EOFFRaw to EGU Offset
EGULEngineer Units Low
EGUFEngineer Units Full
PBRKPointer to brkTable
LBRKLast brkTable
  1. (RVAL + ROFF)
  2. 如果ASLO不为0, 乘以ASLO
  3. 再加上AOFF
  4. 判断LINR的值:
    1. 如果为NO CONVERSION, 结束转换
    2. 如果为SLOPE, 乘以ESLO, 再加上EOFF
    3. 如果为LINEAR, 由special_linconv函数根据用户设置的EGULEGUF, 以及hardware的实际量程来计算出ESLOEOFF, 具体计算规则根据不详.
    4. 其它选项, 使用breakpoint table.

Smoothing Filter

SMOO值为0-1, 0的话代表不使用filter, 1的话代表infinite smoothing, 计算公式如下.

VAL = VAL * SMOO + (1 - SMOO) * New Data

Display

fieldinformation
PRECDisplay Precision
EGUEngineering Units, 16 characters
HOPRHigh Operating Range, 设置一些控件的上限
LOPRLow Operating Range, 设置一些控件的下限
HIHI
LOLO
HIGH
LOW
HHSV
LLSV
HSV
LSV
HYST加入hysteresis, 比如low limit是10, HYST为1, 当val从11降为10.5时无alarm, 但当val从9升为10.5时, 依然触发alarm, 直到vale > 10 + 1时, alarm才消失.
AFTCAlarm Filter Time Constant, 类似与HYST, 不过是当val持续一定时间后再触发alarm.
AFVLAlarm Filter Value, AFTC计算过程中使用的变量.
MDELMonitor Deadband, value monitors, 当val变化超过deadband时才会发送monitor. 0的话每次val改变都触发, -1时record process时触发.
ADELArchive Deadband, archive monitors
LALMLast Value Alarmed
ALSTLast Value Archived
MLSTLast Val Monitored
INITInitialized?

示例

 1record(ai, ai){
 2  field(VAL,  "13")
 3  field(HOPR, "0")
 4  field(LOPR, "100")
 5  field(PREC, "3")
 6  field(HIHI, "90")
 7  field(HIGH, "80")
 8  field(LOW,  "20")
 9  field(LOLO, "10")
10  field(HHSV, "MAJOR")
11  field(HSV,  "MINOR")
12  field(LSV,  "MINOR")
13  field(LLSV, "MAJOR")
14  field(PINI, "RUNNING")
15  field(HYST, "1")
16}

以下为HSYT的示例, 可以看到VAL从13变为21时, 依然有low alarm. ai record alarm


Analog Output Record (ao)

ao的field和ai基本相同. 一些特殊的field:

  • OIF: 可选项为IncrementalFull, 也就是output之前加上PVAL的值.
  • DRVL: Drive Low Limit, 输出的下限
  • DRVH: Drive High Limit 输出的上限
  • OROC: Output Rate of Change, 设置与上次输出的OVAL field相比能改变的最大值.

对于record中的一些preview value, 命名有些奇怪, 有时候是PVAL, 有时候又是OVAL, 即Old value. 在ao record中, OVAL代表Output value, 所以不称呼为Old value可以理解. 但又把ORAW叫Previous Raw Value, 把ORBV叫Previous Readback Value. 真是很让人困惑


Analog Array Input Record (aai)

初始化时可以用支持JSON的link. 引号在使用Relaxed JSON时可以省略, 但规则不详.

MPSTAPST控制何时触发monitor, 他们使用了HASHfield来判断值是否发生变化. 但鉴于可能的哈希碰撞(Hash Collision), 所以最好使用Always, 也就是每次process就会触发monitor.

 1record(aai, aai){
 2  field(NELM, "10")
 3  field(FTVL, "SHORT")
 4  field(APST, "On Change")
 5  field(MPST, "Always")
 6#  field(INP,  "[3,1,1]")
 7  field(INP,  "{const: [315, 10, 0, 0, 1]}")
 8}
 9
10record(aao, aao){
11  field(OMSL, "closed_loop")
12  field(DOL,  "aai")
13  field(PINI, "RUNNING")
14  field(FTVL, "SHORT")
15  field(NELM, "10")
16  field(OUT,  "wf")
17}
18record(waveform, wf){
19  field(FTVL, "LONG")
20  field(NELM, "20")
21}

Analog Array Output Record (aao)

示例见aai


Waveform Record (waveform)

示例见aai

aai多的field: 基本都不再使用了.

  • RARM: Rearm the waveform
  • BUSY: Busy Indicator

Binary Input Record (bi)

COSV可以用来设置value改变时的alarm, 可以设置为MINOR or MAJOR. 如下例, 当value为0时, 触发MAJOR, value从0变1时, 触发MINOR, value从1变1时, 触发NO_ALARM.

device support可以使用MASKfield. 不详.

1record(bi, bi){
2  field(VAL,  "0")
3  field(ZNAM, "OFF")
4  field(ONAM, "ON")
5  field(ZSV,  "MAJOR")
6  field(OSV,  "NO_ALARM")
7  field(COSV, "MINOR")
8  field(PINI, "YES")
9}

Binary Output Record (bo)

可以用HIGHfield来判断ioc是否存活. 如下例, i_am_alive持续写入deadIfZero, 如果使用hardware interrupt来processi_am_alive, 当持续5秒未收到interrupt, 则deadIfZero会因为HIGHfield, 在5秒后自动归零.

 1record(bo, "i_am_alive") {
 2    field(DTYP, "Soft Channel")
 3    field(SCAN, "1 second")
 4    field(DOL, "1")
 5    field(UDF, "0")
 6    field(ZNAM, "Dead")
 7    field(ONAM, "Alive")
 8    field(OUT, "deadIfZero.VAL PP")
 9}
10
11record(bo, "deadIfZero") {
12    field(DTYP, "Soft Channel")
13    field(ZNAM, "Dead")
14    field(ONAM, "Alive")
15    field(HIGH, "$(DEAD_SECONDS=5)")
16}

Calculation Record (calc)

有12个input links.

 1# 简单的例子, heartbeat
 2record(calc, "calc"){
 3  field(VAL,  "0")
 4  field(CALC, "VAL?0:1")
 5  field(SCAN, "1 second")
 6}
 7# 复杂的例子, 产生连续正弦波, 初始相位可调
 8record(calc, "calc"){
 9  field(INPA, "1.57079632679489661923") # 初始相位为pi/2
10  field(CALC, "A:=(A+D2R)>(2*PI)?0:(A+D2R); sin(A)")
11  field(SCAN, ".1 second")
12}

Calculation Output Record (calcout)

相比与calc, 增加了一些output选项.

  • OOPT: Output Execute Opt, 可选Every Time, On Change, When Zero, When Non-zero, Transition To Zero, Transition To Non-zero
  • DOPT: Output Data Opt, 可选CALC, OCAL
  • OCAL: Output Calculation, 和CALC一样包含运算表达式
  • OVAL: Output Value, OCAL的输出结果
  • OEVT: Event To Issue, 要触发的event
  • ODLY: Output Execute Delay, 触发event和输出前的delay
  • DLYA: Output Delay Active, delay过程中这个field会被设为1
  • INAV-INIV: Input PV Status, 会被设为Ext PV NC, Ext PV OK, Local PV, Constant
  • OUTV: Output PV Status, 同上
  • CLCV: CALC Valid,
  • OCLV: OCAL Valid,
  1. 首先计算CALC, 得到的结果放到VAL里;
  2. 然后判断OOPT
    1. 不满足什么也不做;
    2. 如果满足再判断DOPT,
      1. 如果用CALC, 那就把VAL赋值给OVAL
      2. 如果要用OCAL, 那就再计算得到OVAL
    3. OVAL输出到OUT
 1record(calcout, "co"){
 2  field(CALC, "VAL<10?VAL+1:0")
 3  field(OCAL, "90-89")
 4  field(OOPT, "When Zero")
 5  field(DOPT, "Use OCAL")
 6  field(OUT,  "res PP")
 7  field(SCAN, "1 second")
 8}
 9
10record(ao, "res") {
11  field(TPRO, "1")
12}

Compression Record (compress)

两种工作模式, 当input link是数组时, 取所有数据然后计算. 如果不是, 那就每次process一次就把数据加入环形缓冲区进行计算.

使用的field, 其中关键的就是NSAMN, 前者决定compress中存多少数据, 后者决定每多少个输入进行一次计算. ILIL IHIL OFF都只在N to 1类的算法中使用.

  • NSAM: Number of Values
  • N: N to 1 Compression
  • ILIL: Init Low Interest Lim, 过滤数据的下限
  • IHIL: Init High Interest Lim, 过滤数据的上限
  • OFF: Offset, 从哪个数据开始算,
  • RES: Reset

支持的compression algorithm, 也就是ALG field的值:

  • N to 1 Low Value
  • N to 1 High Value
  • N to 1 Average
  • Average
  • Circular Buffer, 保持一个大小为NSAM的缓冲区.
  • N to 1 Median

compress的input link支持array field, 但只支持N to 1类的算法.

BLAG又可选缓冲区的类型, FIFO Buffer 或者 LIFO Buffer

calc每秒自增1, compress每隔10输入计算一次平均值, 最多存两个平均值.

 1record(calc, calc){
 2  field(CALC, "VAL+1")
 3  field(FLNK, "cp PP")
 4  field(SCAN, "1 second")
 5}
 6record(compress, cp) {
 7  field(INP,  "calc NPP")
 8  field(ALG,  "Average")
 9  field(BALG, "LIFO Buffer")
10  field(NSAM, "2")
11  field(N,    "10")
12  field(PINI, "YES")
13}

下例的效果, 计算10个输入的平均值, 然后放到compress的VAL field中. 使用BALG使得新值放到数组最前面.

1$ camonitor cp
2cp                             <undefined> UDF INVALID
3cp                             2024-05-17 16:03:37.379709 4.5
4cp 2024-05-17 16:03:47.379616 2 14.5 4.5
5cp 2024-05-17 16:03:57.379640 2 24.5 14.5

Event Record (event)

用来触发一个epics event, 比如下例会触发event 14.

 1record(event, event){
 2  field(INP, "14")
 3  field(TPRO, "1")
 4}
 5
 6record(calc, calc){
 7  field(CALC, "VAL+1")
 8  field(SCAN, "Event")
 9  field(EVNT, "14")
10  field(TPRO, "1")
11  field(PRIO, "HIGH")
12}
13record(ai, ai){
14  field(INP, "calc")
15  field(SCAN, "Event")
16  field(EVNT, "14")
17  field(TPRO, "1")
18}

如下所示aicalc都被触发了, 但ai先触发, 所以calc变为1, 而ai依然为0.

即使加上PRIO也不影响, 可能与record load时的先后顺序有关, 不详.

所以event适用于无数据流关系的一些record触发, 而强数据流关系的还是用link合适.

1epics> dbpf event.PROC 0
2_main_: dbProcess of 'event'
3DBF_UCHAR:          0 = 0x0
4cbLow: dbProcess of 'ai'
5cbHigh: dbProcess of 'calc'
6# caput event.PROC 0
7epics> ca:sdcswd@zephyrus: dbProcess of 'event'
8cbLow: dbProcess of 'ai'
9cbHigh: dbProcess of 'calc'

Fanout Record (fanout)

fanout有16个输出link.

对于fanout类的record, 有三种SELM, 即Select Mechanism

  • All:
  • Specified: 从SELL得到值, 放到SELN, 然后加上OFFS, 然后触发对应的link. e.g., 1 -> LNK1.
  • Mask: 把SELN右移SHFT位, 对应bit为1的会触发. e.g., 0x01 -> LNK0. 注意SHFT默认值为-1, 也就是默认会把SELN左移一位. 当SELN为2时, bit 3为1, LNK2被触发.

Q: 为什么SHFT默认值为-1?

A: 历史兼容性. 早期fanout的link是从1开始的, 而且只有6个输出link, 当新增LNK0后, 需要增加SHFT来兼容旧的record.

 1record(fanout, fo){
 2  # Specified, LNK1被触发
 3  field(SELM, "Specified")
 4  field(SELN, "0")
 5  field(OFFS, "1")
 6
 7  # Mask, LNK1和LNK2被触发
 8  # field(SELM, "Mask")
 9  # field(SELN, "6")
10  # field(SHFT, "0")
11
12  field(LNK0, "calc0")
13  field(LNK1, "calc1")
14  field(LNK2, "calc2")
15  field(LNK3, "calc3")
16  field(LNK4, "calc4")
17}
18record(calc, calc0){
19  field(CALC, "VAL+1")
20}
21record(calc, calc1){
22  field(CALC, "VAL+1")
23}
24record(calc, calc2){
25  field(CALC, "VAL+1")
26}
27record(calc, calc3){
28  field(CALC, "VAL+1")
29}
30record(calc, calc4){
31  field(CALC, "VAL+1")
32}

Data Fanout Record (dfanout)

有8个输出link.

对比fanout, 多了数据转发, 但是少了bit shift和offset field. 而且link field的命名也变为了OUTA -> OUTH.

  • 对于All, 用于把一个value写到多个record.
  • 对于Specified, 当SELN为0时, 不会输出, 为1时, 输出到OUTA, 和fanout不同.
  • 对于Mask, LSB为1时, 输出到OUTA
 1record(dfanout, dfo){
 2  field(OMSL, "closed_loop")
 3  field(DOL,  "19")
 4
 5  # Specified, SELN为0时无输出
 6  # field(SELM, "Specified")
 7  # field(SELN, "0")
 8
 9  # Mask, 二进制为`101`, 所以输出到OUTA和OUTC
10  field(SELM, "Mask")
11  field(SELN, "5")
12
13  field(OUTA, "calc0")
14  field(OUTB, "calc1")
15  field(OUTC, "calc2")
16  field(OUTD, "calc3")
17  field(OUTE, "calc4")
18}

Histogram Record (histogram)

计算histogram, 用到的field:

  • NELM: Num of Array Elements
  • SVL: Signal Value Location, 数据源
  • SGNL: Signal Value, 数据
  • ULIM: Upper Signal Limit, 低于上限才会被收录
  • LLIM: Lower Signal Limit, 高于下限才会被收录
  • MDEL: Monitor Count Deadband, 何时触发monitor, 与MCNT进行比较, MCNT大于MDEL时才触发monitor, 比如每收集100个数据触发一次monitor.
  • SDEL: Monitor Seconds Dband, 定时检测MCNT, 当MCNT大于0时就触发monitor, 应该是用于数据稀少或非周期性到来的场景.
  • MCNT: Counts Since Monitor, 每次有新数据就加1, 触发monitor之后就置0.
  • CMD: Collection Control, 控制histogram, 清空或者暂停或者恢复读取. Read, Clear, Start, Stop
  • CSTA: Collection Status, 内部状态位, 当它为TRUE时才会采集数据. 只有Start会设置它为TRUE. 而ReadClear都只会清除已采集的数据, 也就是使用Stop的话必须使用Start才能开启采集. 当然也可以只用Clear来重新采集. Read命令和Clear没区别.
  • WDTH: Element Width, 由(ULIM - LLIM) / NELM 计算width, 比如下限为4, 上限12, 四个bin, 那每个bin的WDTH就是2.
 1record(calc, "calc"){
 2  field(INPA, "1.57079632679489661923") # Phase
 3  field(CALC, "A:=(A+D2R)>(2*PI)?0:(A+D2R); sin(A)")
 4  field(SCAN, ".1 second")
 5  field(FLNK, "hist")
 6}
 7
 8record(histogram, hist){
 9  field(SVL,  "calc NPP")
10  field(NELM, "10")
11  field(ULIM, "1")
12  field(LLIM, "-1")
13  field(MDEL, "20")
14  field(SDEL, "0")
15  field(CMD,  "Read")
16}

以下为一个正弦波的直方图示例, 横坐标应该是从-1到1的十组, 但我不知道怎么修改Phoebus的横轴😞. 也许需要一个单独的waveform作为横轴吧. histogram record

另外, 发现了histogram的设计问题, 写入Stop后, 虽然的确停止采集了, 但返回值却依然是Read. 这样就无法使用CMD判断当前状态了. 必须要读取CSTA.

1$ caput hist.CMD "Stop"
2Old : hist.CMD                       Read
3New : hist.CMD                       Read

Long Input Record (longin)

32位整数, 支持AFTC.


Long Output Record (longout)

32为整数, 支持DRVHDRVL.

支持OOPT. 不知为什么, ao, int64out都不支持OOPT.


64bit Integer Input Record (int64in)

longin相似, 只不过支持64位长度.

1record(longin, li){
2}
3record(int64in, i64){
4}

允许超过32位上限

 1$ caput li 2147483647
 2Old : li                             111
 3New : li                             2147483647
 4sdcswd @ zephyrus in  ~/epics/R7.0.8/base [11:47]
 5$ caput li 2147483649
 6Old : li                             2147483647
 7CA.Client.Exception...............................................
 8New : li                             2147483647
 9    Warning: "Channel write request failed"
10    Context: "op=1, channel=li, type=DBR_STRING, count=1, ctx="li""
11    Source File: ../oldChannelNotify.cpp line 159
12    Current Time: Thu May 16 2024 11:47:58.193643892
13..................................................................
14$ caput i64 2147483647
15Old : i64                            0
16New : i64                            2.14748e+09
17sdcswd @ zephyrus in  ~/epics/R7.0.8/base [11:48]
18$ caput i64 2147483999
19Old : i64                            2.14748e+09
20New : i64                            2.14748e+09

64bit Integer Output Record (int64out)

类似longout, 但R7.0.8依然不支持OOPT, 也许将来会加上.


Multi-Bit Binary Input Record (mbbi)

读取32位无符号数或者字符串, 然后匹配对应的值.

因为是从hardware中读取值, 所以类似ai, 也要raw value和value的区分, raw value经过转换然后放入value. 具体转换规则要依据device support而定.

对于16个匹配项, 每个都有一个值, 一个string, 一个severity. 1-16的前缀分别为: ZR, ON, TW, TH, FR, FV, SX, SV, EI, NI, TE, EL, TV, TT, FT, FF. 分别加上VL, ST, SV就可以.

使用的field:

  • NOBT: Number of Bits, 由此得到MASK, ((1 << NOBT) - 1)
  • SHFT: 移位, 对于raw value是右移位, 也会把MASK左移SHFT
  • RVAL: 原始值&MASK的值
  • UNSV: Unknown State Severity, 无匹配时的severity
  • COSV: Change of State Svr

如下例, mbbi的值为VAL&0xf0, 因为设置了SHFT, 所以MASK移位为0xf0. 然后raw value就是原始值与MASK与操作的结果, 也就是0x30. . 然后把RVAL右移4位后进行匹配, 也就是0x3, 与ONVL匹配成功.

可以看到同时使用SHFTNOBT的逻辑显得令人困惑. 所以感觉应该谨慎使用SHFT.

 1record(mbbi, mbbi){
 2  field(NOBT, "4")
 3  field(DTYP, "Raw Soft Channel")
 4  field(INP,  "0x1234")
 5  field(SHFT, "4")
 6  field(ZRVL, "0x4")
 7  field(ZRST, "VAL&0xf")
 8  field(ZRSV, "MINOR")
 9  field(ONVL, "0x3")
10  field(ONST, "VAL&0xf0")
11  field(ONSV, "MINOR")
12  field(FFVL, "0x1234")
13  field(FFST, "All")
14  field(PINI, "RUNNING")
15}

Multi-Bit Binary Output Record (mbbo)

类似于mbbi, 输出32位无符号数. 但是多了output相关的一些field, 比如DOL, OMSL, IVOA, IVOV

例子: 用DOL来决定输出哪个匹配项, 也可以直接caput mbbo 1来选择.

 1record(mbbo, mbbo){
 2  field(DTYP, "Raw Soft Channel")
 3  field(DOL,  "15")
 4  field(OMSL, "closed_loop")
 5  field(PINI, "RUNNING")
 6  field(OUT,  "li")
 7  field(ZRVL, "111")
 8  field(ZRST, "aaa")
 9  field(ZRSV, "MINOR")
10  field(ONVL, "222")
11  field(ONST, "bbb")
12  field(ONSV, "MINOR")
13  field(FFVL, "32768")
14  field(FFST, "fff")
15}
16record(longin, li){}

Multi-Bit Binary Input Direct Record (mbbiDirect)

mbbi是匹配, mbbiDirect则是直接映射/操作bit.

把一个32位 有符号数 的每一位放到B0-B1F中. 当然mbbiDirect主要用于寄存器操作, 也无所谓有无符号.

同样也支持NOBT, MASK, SHFT.

1record(mbbiDirect, mbbidir){
2  #field(NOBT, "4")
3  field(DTYP, "Raw Soft Channel")
4  field(INP,  "0xaaaa")
5  #field(SHFT, "4")
6  field(PINI, "RUNNING")
7}

Multi-Bit Binary Output Direct Record (mbboDirect)

从DOL中取数, 然后输出.

R7.0.6.1开始, 当OMSLclosed_loop时, 禁止直接caput mbbodir.BO 1. 需要先修改成supervisory. 然后直接写B0-B1F这些field. 直接caput mbbo 0xffff无效.

1record(longout, lo){}
2record(mbboDirect, mbbodir){
3  field(PINI, "YES")
4  field(DOL,  "0xaaaa")
5  field(OMSL, "closed_loop")
6  field(DTYP, "Raw Soft Channel")
7  field(OUT,  "lo PP")
8}

Permissive Record (permissive)

deprecated


Printf Record (printf)

用printf来输出格式化字符串. 可以输出到别的string record, 或者使用base提供的device support来输出到stream. 可用@stdout, @stderr or @errlog

SIZV 不能超过32767, 不然会导致segmentation fault.

 1record(printf, pr){
 2  field(DTYP, "stdio")
 3  field(SIZV, "80")
 4  field(FMT,  "0: %s, 1: %f, 2: %f, 3: %s")
 5  field(INP0, {const: hello})
 6  field(INP1, {const: "3.14159"})
 7  field(INP2, {const: "pi:3.14159"})
 8  field(INP3, "non-exist-link")
 9  field(IVLS, "Whoops")
10  field(PINI, "YES")
11  field(OUT,  "@stdout")
12}

Select Record (sel)

12个输入link.

INPA-INPL中得到数据放到fieldA-L中, 然后依据SELM来决定VAL.

  • Specified: 使用SELN中的值(0-11), 决定使用哪个input link的值. 也可以使用NVL这个link来获得SELN. 此处为什么不继续使用SELL这个名称而要改为NVL呢?
  • High Signal: 选择A-L中的最大值
  • Low Signal: 选择A-L中的最小值
  • Median Signal: 选择A-L中的中位数

Sequence Record (seq)

16个输入输出link, 相当于16个ao集成在一起.

DOL0-DOLF得到value放到DO0-DOF中, 然后输出到LNK0-LNKF. 支持SHFTOFFS. 支持delay field, DLY0-DLYF, 在取数之前等待一段时间.

如下例, 会输出10到LNK0, 11到LNK1.

 1record(seq, seq){
 2  field(SELM, "Mask")
 3  field(SELN, "3")
 4  field(SHFT, "0")
 5  field(OFFS, "0")
 6
 7  field(DOL0, "10")
 8  field(DOL1, "11")
 9  field(DOL2, "12")
10  field(DOL3, "13")
11  field(DOL4, "14")
12  field(LNK0, "calc0")
13  field(LNK1, "calc1")
14  field(LNK2, "calc2")
15  field(LNK3, "calc3")
16  field(LNK4, "calc4")
17}

State Record (state)

deprecated


String Input Record (stringin)

40 characters


String Output Record (stringout)

40 characters


Long String Input Record (lsi)

65535 characters

SIZV: 默认41, 不能超过65535

有bug, 当设置SIZV大于32767时, 使用CA读取时会导致ioc segmentation fault.

1record(lsi, lsi){
2  field(SIZV, "32768")
3  field(INP,  {const: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa})
4  field(PINI, "YES")
5}

Long String Output Record (lso)

65535 characters

同样, SIZV大于32767时候就出问题

1record(lso, lso){
2  field(OMSL, "closed_loop")
3  field(DOL,  {const: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb})
4  field(SIZV, "32768")
5  field(PINI, "YES")
6}

Sub-Array Record (subArray)

对于subArray record, 和waveform类似, 都有FTVL, NELM, NORD这些field.

额外的field包括

  • MALM: 需要设置为waveform的size, 分配subArray元素的内存时会用MALM的大小
  • INDX: index, 必须小于MALM.

注意它的实现只能获得前MALM个元素, 也就是说当MALM为1时, index只能为0. 如果要从一个size为1000的waveform中获得最后一个元素, 那subArray的大小也必须为1000, 即使它只关心那最后一个元素.

下例中, 当设置MALM为5, 那waveform中后五个元素就无法得到了.

 1record(subArray,"subarr") {
 2  field(INP,  "wf.VAL NPP NMS")
 3  field(MALM, "5")
 4  field(NELM, "2")
 5  field(INDX, "4")
 6  field(FTVL, "ULONG")
 7}
 8record(waveform,"wf") {
 9  field(PINI, "YES")
10  field(INP,  "[3,1,4,1,5,9,2,6]")
11  field(NELM, "10")
12  field(FTVL, "ULONG")
13  field(FLNK, "subarr")
14}

Subroutine Record (sub)

sub有12个input link.

用法如下, 很简单, INAM函数用来初始化一些设置, SNAM函数会在每次process时调用.

1record(sub,"$(user):subExample")
2{
3  field(INAM,"mySubInit")
4  field(SNAM,"mySubProcess")
5}

Array Subroutine Record (aSub)

aSub就复杂多了, 有20个input和20个output.

使用的field:

  • LFLG: Subr. Input Enable, 是否在每次record process时从SUBL读取新的值, 默认IGNORE, 可选READ. ! 这时候选项又不是首字母大写了. (┬┬﹏┬┬)
  • SUBL: Subroutine Name Link, input link, 可以切换subroutine
  • EFLG: Output Event Flag, 是否触发输出事件, 默认为ON CHANGE, 可选NEVER, ALWAYS
  • VAL: subroutine的返回值, 状态码, 用于判断是否output, 0代表无故障.
  • OVAL: Old return value
  • BRSV: Bad Return Severity, 设置subroutine返回值不为0时的severity.
  • INAM: 初始化时调用
  • SNAM: process调用
  • INPA-INPU: input link
  • A-U: 输入值, 可以为数组
  • FTA-FTU: 数据类型, 输入
  • NOA-NOU: Max. elements, 在subroutine中使用prec->NOT要注意, not是C++的关键字, 所以必须大写来规避.
  • NEA-NEU: Num. elements
  • OUTA-OUTU: output link
  • VALA-VALU: 由subroutine负责修改, 写入到output link
  • OVLA-OVLU: 旧的VALA-VALU值, 用于比较是否变化.
  • FTVA-FTVU: 数据类型, 输出
  • NOVA-NOVU: Max. elements, 输出
  • NEVA-NEVU: Num. elements, in VALA-VALU, 输出
  • ONVA-ONVU: Old Num. elements, in OVLA-OVLU, 输出
1record(aSub,"$(user):aSubExample")
2{
3  field(INAM,"myAsubInit")
4  field(SNAM,"myAsubProcess")
5  field(FTA,"DOUBLE")
6  field(NOA,"10")
7  field(INPA,"$(user):compressExample CPP")
8}