Thirty minutes, and by the end you’ll have records that compute, alarm, and respond over the network — which is the moment EPICS stops being abstract.
Prerequisite: EPICS Base built and on your PATH.
The fastest possible start. No application, no Makefile, no st.cmd.
Create ~/first.db:
# A setpoint an operator would write to
record(ao, "DEMO:Heater-SP") {
field(DESC, "Heater power setpoint")
field(EGU, "%")
field(PREC, "1")
field(DRVL, "0") # writes are CLAMPED to 0-100 by the IOC
field(DRVH, "100") # no client can exceed this
field(PINI, "YES") # push the initial value at startup
}
# A "measurement" that follows the setpoint with lag, standing in for physics
record(calc, "DEMO:Temperature") {
field(DESC, "Chamber temperature")
field(SCAN, "1 second")
field(INPA, "DEMO:Heater-SP") # the setpoint
field(INPB, "DEMO:Temperature") # my own previous value
field(CALC, "B + (20 + A*0.6 - B) * 0.15") # first-order approach
field(EGU, "degC")
field(PREC, "2")
field(HIGH, "60") field(HSV, "MINOR")
field(HIHI, "75") field(HHSV, "MAJOR")
field(MDEL, "0.05") # don't post monitors for changes below this
field(ADEL, "0.5") # don't post archive events for changes below this
}
# Interlock logic: trip if too hot. Note this is DEMONSTRATION logic --
# a real interlock belongs in hardware, not here.
record(calc, "DEMO:OverTemp-Sts") {
field(DESC, "Over-temperature detected")
field(INPA, "DEMO:Temperature CP MS") # CP: process me when A changes
field(CALC, "A > 75") # MS: inherit A's alarm severity
field(SCAN, "Passive")
}
# A summary of everything, for a top-level display
record(calc, "DEMO:Summary") {
field(DESC, "Demo subsystem summary")
field(INPA, "DEMO:Temperature CP MS")
field(INPB, "DEMO:OverTemp-Sts CP MS")
field(CALC, "A")
}
Run it:
softIoc -d ~/first.db
In a second terminal:
caget DEMO:Heater-SP DEMO:Temperature
camonitor DEMO:Temperature DEMO:OverTemp-Sts &
caput DEMO:Heater-SP 50 # watch the temperature climb toward 50
caput DEMO:Heater-SP 100 # watch it pass MINOR at 60, MAJOR at 75
caput DEMO:Heater-SP 200 # clamped to 100 by DRVH
caget DEMO:Heater-SP # 100, not 200
caget -a DEMO:Temperature # value, timestamp, severity, status
Nine things worth noticing:
DRVH clamped your out-of-range write inside the IOC. No client can bypass it. This is the cheapest real protection in EPICS and it’s routinely left unset — see access security.SCAN = 1 second made DEMO:Temperature process on a clock, with no code.CALC with INPB pointing at its own VAL gave you state — a first-order lag filter in one field.CP on DEMO:OverTemp-Sts’s input means it processes when the temperature changes, not on a timer. Event-driven, zero polling. This is the idiom to internalise.MS propagated the alarm severity up the chain, so DEMO:Summary inherits the worst severity of its inputs. That’s how a facility-wide summary display works.camonitor, a screen, the archiver and the alarm system all see the same alarm. A threshold on a screen would be invisible to all of them.MDEL suppressed network traffic for tiny changes. Multiply by 400 000 records to see why it matters.caget -a showed severity. caget alone would have shown you a number with no indication that it was in alarm — the most common client bug.PINI pushed the initial setpoint at startup. Without it, the value exists but has never been written anywhere.Poke at it from the IOC console:
epics> dbl # all four records
epics> dbpr DEMO:Temperature 1 # every field, level 1
epics> dbpr DEMO:Temperature 4 # everything, including links
epics> dbtr DEMO:OverTemp-Sts # test-process it and show the result
epics> casr 1 # who's connected? (your camonitor)
epics> dbgf DEMO:Temperature.SEVR
dbpr is the tool you’ll use for the rest of your career.
softIoc is genuinely fine for a lot of production work. But you need a built application to load device-support modules, and every facility IOC is one, so build one now.
mkdir -p ~/EPICS/iocs/demo && cd ~/EPICS/iocs/demo
makeBaseApp.pl -t ioc demo
makeBaseApp.pl -i -t ioc demo # answer "demo" when prompted for the app
What you get:
demo/
├── configure/
│ ├── RELEASE ← EPICS_BASE and every support module's path
│ └── CONFIG_SITE
├── demoApp/
│ ├── Db/ ← put your .db here, and list it in Makefile
│ └── src/ ← C/C++ and the "DBD includes" list
├── iocBoot/iocdemo/
│ ├── st.cmd ← the startup script
│ └── envPaths ← generated; sourced by st.cmd
└── Makefile
Add your database:
cp ~/first.db demoApp/Db/demo.db
Tell the build about it — in demoApp/Db/Makefile, in the section that installs databases:
DB += demo.db
Build:
make
Now edit iocBoot/iocdemo/st.cmd. The generated file has the structure; make sure it loads your database:
#!../../bin/linux-x86_64/demo
< envPaths
cd "${TOP}"
dbLoadDatabase "dbd/demo.dbd"
demo_registerRecordDeviceDriver pdbbase
## Load the database
dbLoadRecords("db/demo.db")
cd "${TOP}/iocBoot/${IOC}"
iocInit
## Anything after iocInit runs once the IOC is up
Run it:
cd iocBoot/iocdemo
chmod +x st.cmd
./st.cmd
Same PVs, now served by an executable you built and can extend with device support.
The order is not arbitrary:
| Phase | What happens |
|---|---|
Before iocInit |
Load DBDs, register drivers, configure ports (drvAsynIPPortConfigure), load databases, set up autosave paths and pass-0 restore, load access security |
iocInit |
Records initialise, device support connects, scan tasks start, the CA/PVA servers begin serving |
After iocInit |
Autosave monitor sets, sequencer programs, anything needing running records |
Putting something on the wrong side of iocInit is a classic failure: dbLoadRecords after iocInit does nothing useful, and create_monitor_set before it fails.
Real databases are templates. Rewrite demo.db as demo.template:
record(ao, "$(P)$(R):Heater-SP") {
field(DESC, "$(DESC=Heater power)")
field(EGU, "%")
field(DRVL, "0")
field(DRVH, "$(MAXPOWER=100)")
field(PINI, "YES")
}
record(calc, "$(P)$(R):Temperature") {
field(SCAN, "1 second")
field(INPA, "$(P)$(R):Heater-SP")
field(INPB, "$(P)$(R):Temperature")
field(CALC, "B + (20 + A*0.6 - B) * 0.15")
field(EGU, "degC")
field(HIHI, "$(TRIP=75)") field(HHSV, "MAJOR")
}
Instantiate three chambers, from st.cmd:
dbLoadRecords("db/demo.template", "P=DEMO,R=:CH1,TRIP=75")
dbLoadRecords("db/demo.template", "P=DEMO,R=:CH2,TRIP=80")
dbLoadRecords("db/demo.template", "P=DEMO,R=:CH3,TRIP=65,MAXPOWER=50")
Or with a .substitutions file and msi/dbLoadTemplate for larger sets — see templates and substitutions. This is how 176 BPMs get instantiated, and it’s the difference between a database you maintain and a database you generate.
| Next step | Page |
|---|---|
| Put a screen on it | Phoebus or pip install pydm |
| Talk to real hardware | Talk to a Real Device — one instrument, end to end |
| Survive a restart | autosave |
| Understand what you just wrote | Process Database |
| Add IOC health PVs | iocStats |
| Fake a device to talk to | Lewis or pcaspy |
| Do it properly, at scale | Example Facility |
| Symptom | Cause |
|---|---|
Record type "calc" not found |
The DBD wasn’t loaded, or the module providing that record type isn’t in configure/RELEASE |
| Records load but never update | SCAN is Passive (the default) and nothing is triggering them |
st.cmd: Permission denied |
chmod +x st.cmd |
envPaths: No such file |
Run st.cmd from iocBoot/iocdemo/, not from anywhere else — relative paths resolve against the working directory |
Value reads 0 with severity INVALID |
The record processed and the read failed. Look at the device support, not the record. |
caput appears to work but the value doesn’t change |
Clamped by DRVL/DRVH, blocked by access security, or the record is DISAbled |
| Two IOCs, one PV name | You started softIoc twice. Clients bind to whichever answers first. Kill one. |