A bare I2C_master.read returns whatever register the device's internal pointer already addresses. Write the register number first as a one-byte write, read two bytes, gate on a returned length of 2, and pack the word high byte first. On a UPS HAT at 7-bit address 16#36 that reproduces the word i2cget prints at the terminal.
A bare I2C_master.read returns whatever register the device's internal pointer already addresses, and that is why i2cget shows a clean word while the function block shows nothing useful. i2cget writes the register number to the device first, as a one-byte write, and reads only after that. Put the same pointer write in the block, pass every buffer with ADR(), gate on the number of bytes the read returns, and pack the word high byte first. On a UPS HAT at 7-bit address 16#36, those four moves turn a dead block into the exact word the terminal prints.
What you need
- A CODESYS project whose runtime runs on a Linux target with an I2C bus, and the bus number. Bus 1 in the case below.
- The i2c library. In the Library Manager, Add Library, search for i2c, add it.
- An I2C master in the Devices tree: right-click I2C, Add Device, I2C master. Raw register access needs no devicedesc.xml and no device library.
- The device datasheet: the 7-bit slave address, the register map, the byte order of multi-byte registers, the scale factor, and the write-cycle time.
- Terminal access to the same target, for the i2cget reference read and the cross-checks later.
Take the reference reading at the terminal first
- Run
i2cget -y 1 0x36 0x04 w. The1is the bus,0x36the 7-bit address,0x04the register, andwasks for a 2-byte word. Drop thewfor a 1-byte register. - Add
-fwhen a kernel driver already claims the address. - Read the result.
0x7c4dcame back in the case this article follows, so the wiring, the address and the bus number are good. Write that word down: the function block has to reproduce it. If the command fails, fix the hardware, the address or the bus before any PLC code. A function block cannot succeed where i2cget fails.
I2C is an open-drain bus, so the terminal tools and the runtime can both use it while the PLC runs. That gives you a live cross-check in both directions: write with i2cset and read the value back in the block, then write from the block and read it back with i2cget.
Confirm the address in the datasheet before any i2cset line. A write to the wrong address lands on some other chip on the same bus and changes one of its registers.
The function block: pointer write, then read
Declare the block's data like this:
usiAddress : USINT := 16#36;the 7-bit slave addressbyReg : BYTE := 16#04;the register pointerbaBuffer : ARRAY[0..1] OF BYTE;the two data bytesnLen : DINT;how many bytes the read returnedwRaw : WORD;the packed wordrValue : REAL;the scaled engineering value
Then, in this order:
- Put the register number in
byReg. The pointer is one byte on this device, from the register map. - Send it as a one-byte write:
I2C_master.write(usiAddress, ADR(byReg), 1);. This sets the device's internal pointer. - Read the data as its own transaction:
nLen := I2C_master.read(usiAddress, ADR(baBuffer), 2); - Gate every later line on
nLen = 2. Both bytes arrived. Any other value means the device did not acknowledge or the transfer came back short, so check the address form, the bus selected in the I2C master device, and the pull-up resistors. - Pack the word high byte first:
wRaw := MEM.PackBytesToWord(baBuffer[1], baBuffer[0]);. The first byte the device sends is the low byte, the same conventioni2cget wreports. - Scale per the datasheet. The voltage register on this HAT counts 78.125 µV each, so
rValue := wRaw * 78.125 / 1000000;gives volts.
Pass buffers by pointer, always. I2C_master.write(16#68, 16#15, 1) hands the value 16#15 to the runtime where it expects a memory address, so no register byte reaches the bus. Declare the register as a BYTE variable and pass ADR(byReg).
Both calling styles you will meet in example code reach the same master: the positional form above, and the named form (usiAddress :=, pbyBuffer :=, nBufferLen :=) inside a block declared EXTENDS i2c, which calls SUPER^() at the top of its body.
Two calls, two transactions. A minority of devices want one combined transaction with a repeated start between the pointer write and the read, and refuse the two-stop form; the timing section of the datasheet says which kind you have.
Example: reading the UPS word at 16#36, register 16#04
Inputs:
- Bus 1, 7-bit address 16#36 and register 16#04, from the working
i2cget -y 1 0x36 0x04 wcommand. - Read length 2 bytes, because the command ends in
w. - Terminal reference 0x7c4d, taken before any PLC code ran.
- 78.125 µV per count, the datasheet scale for the voltage register 16#02.
Rule in words: one write carrying the single pointer byte, one read of two bytes, a gate on the returned length, and a pack with the high byte first.
Arithmetic:
- The pointer write
I2C_master.write(16#36, ADR(byReg), 1)withbyReg := 16#04puts 04 on the bus and stops. - The read returns
nLen = 2, withbaBuffer[0] = 16#4DandbaBuffer[1] = 16#7C, because the low byte arrives first. wRaw := MEM.PackBytesToWord(baBuffer[1], baBuffer[0])gives 16#7C4D = 124 × 256 + 77 = 31821.
Result: wRaw holds 16#7C4D, the same word the terminal printed, so the transaction matches the reference end to end.
Decision: change byReg to 16#02 and rerun the same six steps for the voltage. The datasheet says whether that register is one scaled word or an integer byte plus a fraction byte. If your block returns 16#4D7C the two pack arguments are swapped; 0 means the read never completed, so go back to step 4.
Check it worked
| What you see | What it points to | What to do |
|---|---|---|
| 0, or one constant value whatever the conditions | the pointer write is missing, so the device serves its default register | add the one-byte write in step 2 |
| Right size, wrong number: 16#4D7C against 16#7C4D | the word was packed low byte first | swap the two PackBytesToWord arguments |
nLen anything except 2 |
no acknowledge: shifted 8-bit address, wrong bus in the I2C master device, weak pull-ups | pass the 7-bit form, fix the bus setting, check the pull-ups |
Then leave the PLC running and probe the same register with i2cget. The bus is open-drain, so both can work at once, and the two readings must match at the same moment. That comparison catches a block whose pointer write runs only on the first call: the device then serves whatever register the last transaction left.
Write a register in one transaction
A register write is a single call whose buffer carries the register byte first and the data bytes behind it:
abWrite : ARRAY[0..2] OF BYTE;for a 16-bit value.abWrite[0] := 16#15;the register.abWrite[1] := DWORD_TO_BYTE(dwValue);the low data byte.abWrite[2] := DWORD_TO_BYTE(SHR(dwValue, 8));the high data byte.I2C_master.write(16#68, ADR(abWrite), 3);one transaction, length 3: 1 register byte plus 2 data bytes.
Swap positions 1 and 2 in the buffer when the datasheet wants the high data byte first. The same mapping turns any i2cset block write into a call: a line that sends 13 data bytes after register 0x02 takes one buffer of 14 bytes and a length of 14.
Three mistakes break writes:
- Register in one write and data in a second. The second transaction's first byte lands in the device as another register address.
- A buffer declared as an array of WORD and filled with byte values. Every element occupies 2 bytes, and on a little-endian target 16#15 leaves as 15 00, shifting every byte behind it.
- A read-back issued immediately after the write. Parts with an internal write buffer stay busy for the datasheet's write-cycle time, several ms on EEPROM-style devices, and drop the read until it elapses.
Raw calls or a device description
Raw calls fit one or two simple register maps: this UPS HAT, a temperature sensor. A device with dozens of registers and a power-up configuration sequence is better served by a device description, where the register map is written once instead of at every call site. Watch for the moment a third configuration register joins the code; that is the move point.
Recommendation: read simple register-mapped devices with the pointer-write-then-read pattern above, gate every read on a returned length of 2, and keep the byte order in one packing call; move to a device description when the device needs a multi-register configuration sequence at power-up.
Once rValue comes out in real units, put it where people act on it: display the value in a visualization puts the number on the operator page, and log the variables to a database keeps the history. If the number belongs on a screen the whole floor reads, live factory data display: signage or dashboard settles which kind of screen that is.
Questions
Why does i2cget return a word while the function block returns nothing useful?
i2cget sends the register number in a one-byte write before it reads; the block's bare read returns whatever register the device's internal pointer already addresses. Leave the register number off the i2cget command and it behaves the same way, reading the currently active register. Add the pointer write and both sides agree.
Do I pass I2C_master the 7-bit address or the shifted 8-bit one?
The 7-bit form. For a datasheet slave address of 0x36 that is 16#36. Datasheets that print the 8-bit write frame 0x6C expect you to shift right one bit: 0x6C shifted right once is 0x36. The runtime builds the frame and the read/write bit itself, so a shifted address points the master at a device that is not on the bus.
My device uses 16-bit register addresses. Does the same pattern work?
Yes, with a longer pointer. Put both register-address bytes in the pointer write, high byte first on the common parts per the datasheet, and pass 2 as the write length. The read that follows is unchanged. Scale factors belong to one register map only, so take every constant like 78.125 µV per count from your own datasheet.
Why does my read return the right value only the first time?
The pointer write ran once and the read runs every cycle. The device keeps its internal pointer, so after any other transaction the pointer sits elsewhere and the bare read serves that register instead. Put the pointer write immediately before the read in the same code path, executed every cycle, and the value stays stable.
Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.