提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言(不想看细节请直接移步 ->总结)
最近做的项目是关于心电ECG和呼吸阻抗的数据采集,用到的是TI的传感器ADS1292R。而心电和呼吸阻抗数据是分别以由两个有符号24位(3个字节)的形式传回单片机,但是上位机程序的变量类型都是2字节或者4字节的(需要先转换成4字节的数据类型才能参与运算),上位机也没有由3字节直接变成4字节的有符号数据类型的转换函数,采用直接强制转换的话又会导致数据错误。
针对此问题,本文的提出了一个基于C语言实现3字节有符号整数向4字节有符号整数转换的方法,这个方法简单高效,经过实测在Dev c++编译器和单片机上应用都没有问题。
一、原码反码补码知识回顾
我们知道,数据在程序里都是以补码的形式存储的,这样对于减法运算也能看成加上其负数的加法,极大地减少复杂电路设计的开销。有关原码反码补码知识可参考这个链接:C语言——原码, 反码, 补码 详解
正数的补码、反码、补码都是其本身;
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反;负数的补码是在反码的基础上再+1。
而对于3字节的ECG数据来说,其也是以补码的形式传回来的,其最高位(第24位)是其符号位。例如,对于+1的24位的原码反码补码表达都为0x000001;而-1的24位表达则是——原码:0x800001;反码:0x ff ff fe;补码:0x ff ff ff。
也就是说,若单片机接收到3字节的数据为0x ff ff ff,则代表数据的取值为-1,通过DAC原理便可反推出ECG信号的电压值了。
二、有符号3字节数据强制转成4字节数据会有什么错误?
对于24位的正数(0 ~ 2^23-1),采用直接强制转换(在前面拼接上一个字节数据0x00)的方式,转换成四字节的数据,结果是没问题的,因为正数的原码、反码、补码都相同;
#include<stdio.h>
int main()
{
unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位
int y; // y是转换后的4字节有符号数据
// 对于24位数+1的表示方法:
// 原码0x 00 00 01
// 反码0x 00 00 01
// 补码0x 00 00 01
x2 = 0x00;
x1 = 0x00;
x0 = 0x01;
y = 0x00<<24 | x2<<16 | x1<<8 | x0;
printf("y=%d\n", y); // 输出:y=1 ;与实际情况相符
return 0;
}
但是对于24位的负数(-2^23 ~ 0),采用直接强制转换(在前面拼接上一个字节数据0x00)的方式,转换成四字节的数据,结果就有问题了:
#include<stdio.h>
int main()
{
unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位
int y; // y是转换后的4字节有符号数据
// 对于24位数-1的表示方法:
// 原码0x 80 00 01
// 反码0x ff ff fe
// 补码0x ff ff ff
x2 = 0xff;
x1 = 0xff;
x0 = 0xff;
y = 0x00<<24 | x2<<16 | x1<<8 | x0;
printf("y=%d\n", y); // 输出:y=16777215 ;与实际情况不符
return 0;
}
这也很容易想得通,因为从三字节扩充到四字节的有符号数据时,三字节数据的符号位会被四字节数据吞并为数据位,这直接关系到数据的正负取值性质。
三、将24位数据扩充为32位数据的方法(不改变数值和符号)
那么怎么样完成数据的正负继承,且不会改变数值呢?
做法一:我们在转换上面的负数时,若把拼接在前面的一个字节数据0x00换成0xff,转换成四字节的负数数据就又对得上了(这跟负数补码的生成有关):
#include<stdio.h>
int main()
{
unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位
int y; // y是转换后的4字节有符号数据
// 对于24位数-1的表示方法:
// 原码0x 80 00 01
// 反码0x ff ff fe
// 补码0x ff ff ff
x2 = 0xff;
x1 = 0xff;
x0 = 0xff;
y = 0xff<<24 | x2<<16 | x1<<8 | x0;
printf("y=%d\n", y); // 输出:y=-1 ;与实际情况相符!
return 0;
}
这说明,可以通过判断24位最高位的符号位来选择在前面拼接0x00还是0xff,以实现3字节数据格式向4字节的数据格式转换。
当然,这是比较笨的做法。为什么说这种做法笨呢?看看下面这个方法你就会不禁感叹原来可以更加巧妙地实现:
#include<stdio.h>
int main()
{
unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位
int y; // y是转换后的4字节有符号数据
// 对于24位数-1的表示方法:
// 原码0x 80 00 01
// 反码0x ff ff fe
// 补码0x ff ff ff
x2 = 0xff;
x1 = 0xff;
x0 = 0xff;
y = (char)x2<<16 | x1<<8 | x0;
printf("y=%d\n", y); // 输出:y=-1 ;与实际情况相符!
return 0;
}
为什么呢,我们来看看进行(char)强制转换后各个变量的十六进制打印:
#include<stdio.h>
int main()
{
unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位
int y=0; // y是转换后的4字节有符号数据
// 对于24位数-1的表示方法:
// 原码0x 80 00 01
// 反码0x ff ff fe
// 补码0x ff ff ff
x2 = 0xff;
x1 = 0xff;
x0 = 0xff;
y = (char)x2<<16 | x1<<8 | x0;
printf("y = %8x , %d\n", y, y); // 输出:y = ffffffff , -1 与实际情况相符
printf("(char)x2<<16 = %8x , %d\n", (char)x2<<16, (char)x2<<16); // 输出:(char)x2<<16 = ffff0000 , -65536
printf("(char)x1<<8 = %8x , %d\n", (char)x1<<8, (char)x1<<8); // 输出:(char)x1<<8 = ffffff00 , -256
printf("(char)x0 = %x , %d\n", (char)x0, (char)x0); // 输出:(char)x0 = ffffffff , -1
printf("x2<<16 = %8x , %d\n", x2<<16, x2<<16); // 输出:x2<<16 = ff0000 , 16711680
printf("x1<<8 = %8x , %d\n", x1<<8, x1<<8); // 输出:x1<<8 = ff00 , 65280
printf("x0 = %8x , %d\n", x0, x0); // 输出:x0 = ff , 255
return 0;
}
可以看到0xff经过(char)强制转换后,不左移其16进制竟然拓展到了32位(为了确认不是打印格式的问题,我还特意把中间那行%8x的8去掉),取值为0xffffffff ! 而左移8位则变成0xffffff00,左移16位变到0xffff0000;
没有进行强制转换的无符号数据左移虽也会拓展,但是其前面并只会补0;而进行(char)x2强制转换的输出y的高八位会跟x2的最高位(符号位)始终保持相同!
再举几个例子,收到-65536与+65536的补码:
#include<stdio.h>
int main()
{
unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位
int y=0; // y是转换后的4字节有符号数据
// 对于24位数-65536的表示方法:
// 原码0x 81 00 00
// 反码0x fe ff ff
// 补码0x ff 00 00
x2 = 0xff;
x1 = 0x00;
x0 = 0x00;
y = (char)x2<<16 | x1<<8 | x0;
printf("y = %8x , %d\n", y, y); // 输出:y = ffff0000 , -65536 与实际情况相符
printf("(char)x2<<16 = %8x , %d\n", (char)x2<<16, (char)x2<<16); // 输出:(char)x2<<16 = ffff0000 , -65536
printf("(char)x1<<8 = %8x , %d\n", (char)x1<<8, (char)x1<<8); // 输出:(char)x1<<8 = 0 , 0
printf("(char)x0 = %8x , %d\n", (char)x0, (char)x0); // 输出:(char)x0 = 0 , 0
printf("x2<<16 = %8x , %d\n", x2<<16, x2<<16); // 输出:x2<<16 = ff0000 , 16711680
printf("x1<<8 = %8x , %d\n", x1<<8, x1<<8); // 输出:x1<<8 = 0 , 0
printf("x0 = %8x , %d\n", x0, x0); // 输出:x0 = 0 , 0
// 对于24位数+65536的表示方法:
// 原码0x 01 00 00
// 反码0x 01 00 00
// 补码0x 01 00 00
x2 = 0x01;
x1 = 0x00;
x0 = 0x00;
y = (char)x2<<16 | x1<<8 | x0;
printf("y = %8x , %d\n", y, y); // 输出:y = 10000 , 65536 与实际情况相符
printf("(char)x2<<16 = %8x , %d\n", (char)x2<<16, (char)x2<<16); // 输出:(char)x2<<16 = 10000 , 65536
printf("(char)x1<<8 = %8x , %d\n", (char)x1<<8, (char)x1<<8); // 输出:(char)x1<<8 = 0 , 0
printf("(char)x0 = %8x , %d\n", (char)x0, (char)x0); // 输出:(char)x0 = 0 , 0
printf("x2<<16 = %8x , %d\n", x2<<16, x2<<16); // 输出:x2<<16 = 10000 , 65536
printf("x1<<8 = %8x , %d\n", x1<<8, x1<<8); // 输出:x1<<8 = 0 , 0
printf("x0 = %8x , %d\n", x0, x0); // 输出:x0 = 0 , 0
return 0;
return 0;
}
总结
正数的补码、反码、补码都是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反,负数的补码则是在反码的基础上再+1。
在程序存储以及数据传输中,数据常以补码的形式出现。
3字节有符号数据采用拼接的方式强制转换变成4字节数据时,若想保存数值与符号都不变,不能直接在高八位加上0x00或者0xff;需要根据三字节的符号位(第24位)进行判断,为0则拼接上0x00,为1则拼上0xff。
可以将三字节数据的最高字节数据进行有符号的强制类型转换(char),强制转换后会拓展出一个四字节的操作数,且比符号位高的高位数据会自动跟符号位取值一样,在这个基础上再与低字节相拼接,这样做就能简单巧妙地,在不改变24位数据正负和取值的条件下完成32位数据的扩展。
简洁有效代码段:
y = (char)x2<<16 | x1<<8 | x0; // 三字节拼接扩展成四字节
#include<stdio.h> int main() { unsigned char x2,x1,x0; // x2、x1、x0分别对应24位数的23~16、15~8、7~0位 int y; // y是转换后的4字节有符号数据 // 对于24位数-1的表示方法: // 原码0x 80 00 01 // 反码0x ff ff fe // 补码0x ff ff ff x2 = 0xff; x1 = 0xff; x0 = 0xff; y = (char)x2<<16 | x1<<8 | x0; // 三字节拼接扩展成四字节 printf("y=%d\n", y); // 输出:y=-1 ;与实际情况相符! return 0; }