-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
modbus_get/set_float_xxxx() mixing bytes #665
Comments
Could you please give some more easy reproducible example and more details of the counterpart device? Would like to verify your observation on my hardware zoo ... |
I think same problem hit my project. It's quite easy to reproduce I guess. Not sure I understand this correctly, but run a float by modbus_set_float_badc() && modbus_get_float_badc() should give same float value used at the beginning ?
Gives in result:
Result is the same for x86_64 and aarch64. |
the current implementation is obviously wrong. i did a short bisect on this. following commit breaks things: 49af73d is the first bad commit
src/modbus-data.c | 110 ++++++++++++++++++++++++++++++++++++++--------- |
in detail it looks to me, that the modbus_set_float functions are not correct |
Hi, I can assert that the current implementation is wrong. I stumbled across this when checking conversion from/to cdab float format. Simple test code: float val = 24;
// on x86 machines in little-endian format (dcba) this is in memory "00 00 c0 41"
// 4 byte memory array with two registers/words, each uint16_t also stored in little endian format
uint16_t reg[2];
modbus_set_float_cdab( val, reg );
// we expect memory of reg to hold "00 00 41 c0" and this is the case, so the implementation of the
// setter function is correct
// backwards
float val2 = modbus_get_float_cdab( reg );
// memory of val2 is now still "00 00 41 c0", in fact ab and cd get got swapped twice Explanation: consider the function implementations
The core problem is the usage of uint16_t and the assumption made when using the shift operation. The fix is simple, and can be combined with a general code cleanup in this file. The old, outdated swap16 and swap32 macros can go, as the useless memcpy calls. I'll submit a patch/pull request to fix the issue, shortly. Note, however, that there is a general problem with these encoding functions. For a cross-platform library like libmodbus, we have to address two memory layouts:
The current function declaration only addresses the target format with the suffix (abcd cdab etc.), yet not the source format. Hence, we need to have separate implementations for systems with big and little endian source floats (switched via compiler defines). |
Example for a litte-endian/big-endian compatible variant. Consider the following code: void modbus_set_float_cdab(float f, uint16_t *dest) {
// interpret memory of float (32 bit) as memory of uint32
uint32_t * i_ptr = (uint32_t*)(&f);
uint32_t i = *i_ptr;
// or short
//
// uint32_t i = *(uint32_t*)(&f);
//
// explaining the above line would be a nice exam question
// for second grade computer science class... :-)
// extract bytes - code that works for both big and little endian
// MIND: shift operations consider Endianess!!!
uint8_t a, b, c, d;
a = (i >> 24) & 0xFF; // on little endians Byte 4 in memory, on big endians: Byte 1 in memory
b = (i >> 16) & 0xFF;
c = (i >> 8) & 0xFF;
d = (i >> 0) & 0xFF;
// alternatively access memory positions directly - this only works for little endian
// since it explicitely specifies source memory locations
uint8_t * in = (uint8_t *)(&f);
a = in[3];
b = in[2];
c = in[1];
d = in[0];
// write into byte buffer - here, we MUST directly write into byte array
// to get the exact pattern as requested
uint8_t * out = (uint8_t *) dest;
out[0] = c;
out[1] = d;
out[2] = a;
out[3] = b;
} Using the first variant (cast to integer and shift) appears to work on both big and little endian, so this might be the way to go. |
Hi, just noticed, I was working with wrong assumption, so my fix needs to be revised first. Consider the transfer from modbus buffer to 16-bit registers: (modbus.c:1311)
Here, the little/big endianess of the host architecture is already considered (the high byte is first in byte stream, then the low byte. The destination uint16 is composed using shift operations, so that the compiler takes care of the actual number representation). So we have the byte order in memory already matching in each 16-bit register. Similarly, when encoding the modbus message, the conversion to big-endian modbus stream is handled already (modbus.c:1515)
I'll revise my patch and submit again. |
Hi, here's a variant. What do you think? /* Get a float from 4 bytes (Modbus) without any conversion (ABCD) */
float modbus_get_float_abcd(const uint16_t *src)
{
/* Suppose the modbus byte stream contained the 32-bit float 0x10203040 - abcd.
On big endian systems, the memory starting at src contains two 16-bit registers 0x1020 and 0x3040
On little endian system, the 16-bit registers memory holds 0x2010 and 0x4030
(mind, not the _number_ 0x2010, but the actual memory content).
To convert the data to float32 on big-endian we only need to cast the pointer and we are done.
On little endian systems, we need to swap the bytes in each word again and then assemble
an integer via shift operations and finally cast to float32.
A portable way is to retrieve low and high bytes of both words using
shift operations, then assemble the 32-bit integer.
*/
float f;
uint8_t a, b, c, d;
// access buffer memory byte-wise ABCD
a = src[0] >> 8; // high byte of first word
b = src[0] & 0x00ff; // low byte of first word
c = src[1] >> 8; // high byte of second word
d = src[1] & 0x00ff; // low byte of second word
// assemble in memory location of float
// from right to left: get address of float, interpret as address to uint32, dereference and write uint32
*(uint32_t *)&f = (a << 24) | (b << 16) | (c << 8) | (d << 0);
return f;
} |
Did anyone check my patch, yet? I'd be happy to get some feedback... |
I will check it in next few days. |
Hi again, any progress on the issue? I'd appreciate if we introduce a solution to this bug into the current libmodbus, before it gets included in any linux distro packages and breaks existing tools. Any way I could help? |
Hmmm, I can't say, If I got you right. The "Modubus Application Protocol V1.1b" specification says in section 4.2 (cite:) "MODBUS uses a ‘big-Endian’ representation for addresses and data items. This means that when a numerical quantity larger than a single byte is transmitted, the most significant byte is sent first. So for example
." |
Hi, you are of course right, regarding the modbus protocol specs: here it is clearly stated, that big endian is to be used. BUT, in practice, we are sometimes dealing with IOT devices, sometimes developed by small companies with their own tool sets and often enough we have float/4 byte integer encodings that do not follow protocol. As long as the byte order in the modbus stream is known, we can just use one of the 4 provided functions and be done. For modbus protocol conform devices, always abcd is the right choice. So the question is rather: should libmodbus support non-standard modbus implementations, or should we only stick to proper modbus protocol. If we decide for the latter, we can drop all functions except abcd and just use the old, single modbus get/set function. @stephane: what's your take on the issue? |
My take is libmodbus should support non-standard Modbus implementations because there are sadly useful. |
After a lot of compile runs for different version I'm now quite sure there is a change in implementation of modbus_get_float_abcd() and modbus_set_float_abcd() funtions, probably all 8 variants are affected.
libmodbus version
branch v3.1.8 sources from github
OS and/or distribution
Linux Kernel 3.10 with Debian 11
Environment
Linux Debian 10 crosscompile for embedded arm with ./configure --host=arm-linux-gnueabihf
Description
Expected behavior or suggestion
Up to version 3.1.7 all 8 functions are working as expected and how I understood the documentation
The text was updated successfully, but these errors were encountered: