3. 数据类型与位宽:标准整型、浮点型陷阱、枚举与联合体、位域的精打细算

说到数据类型,很多刚入行的朋友觉得这有什么好讲的?不就是int、char、float吗?嗯,在MCU上,事情可没那么简单。我见过太多因为数据类型选错,导致程序跑飞、内存溢出、甚至芯片直接锁死的案例。今天咱们就好好掰扯掰扯。

3.1 标准整型:别再乱用int了

我个人习惯,在MCU开发中几乎不用原生的int。为什么?因为不同编译器、不同芯片,int的位宽可能不一样。在8位单片机上int是16位,在32位ARM上int是32位。你写个int a,换个平台就翻车。

所以,我建议你养成一个习惯:只用stdint.h里定义的类型

#include <stdint.h>

int8_t   a;   // 8位有符号
uint8_t  b;   // 8位无符号
int16_t  c;   // 16位有符号
uint16_t d;   // 16位无符号
int32_t  e;   // 32位有符号
uint32_t f;   // 32位无符号

这样做的好处很明显:代码可移植性高,位宽一目了然。我在项目中遇到过有人用int存传感器数据,结果换了个芯片后数据直接溢出,排查了整整两天才发现是类型位宽变了。

核心原则:在MCU上,永远明确指定变量的位宽。不要依赖编译器的默认行为。

3.2 浮点型陷阱:能不用就别用

浮点型在MCU上是个大坑。很多低端MCU根本没有硬件浮点单元(FPU),所有浮点运算都要靠软件模拟。一次浮点加法可能消耗几百个CPU周期,而整数加法只需要1个周期。

为什么会这样?因为软件浮点库要处理指数对齐、尾数运算、规格化等一系列操作。你想想看,在8MHz的8位单片机上跑浮点运算,那画面太美我不敢看。

我曾经在一个项目中,用float存温度值,结果主循环里做了一次浮点除法,整个系统的响应时间从1ms变成了50ms。后来改成定点数,问题立刻解决。

避坑指南:我曾经在STM32F103上做过一个PID控制项目,用了double类型。结果发现运算速度慢得离谱,查手册才知道F103没有硬件双精度FPU。换成float后速度提升了一倍。所以,如果非要用浮点,优先选float,别碰double

那浮点真的不能用吗?也不是。如果你用的是带FPU的芯片(比如STM32F4系列),单精度float运算速度很快。但要注意:浮点比较时别用等号

// 错误写法
if (a == 0.1f) { ... }

// 正确写法
#define EPSILON 0.0001f
if (fabs(a - 0.1f) < EPSILON) { ... }

3.3 枚举与联合体:省RAM的神器

枚举(enum)在MCU上特别好用。它本质上就是个整数,但能让代码可读性大大提高。

typedef enum {
    STATE_IDLE = 0,
    STATE_RUNNING,
    STATE_ERROR,
    STATE_SLEEP
} system_state_t;

system_state_t current_state = STATE_IDLE;

你看,用枚举比用#define清晰多了。而且编译器会帮你检查类型,减少出错概率。不过要注意:枚举的底层类型默认是int,在8位MCU上可能浪费空间。我建议显式指定底层类型:

typedef enum : uint8_t {
    STATE_IDLE = 0,
    STATE_RUNNING,
    STATE_ERROR,
    STATE_SLEEP
} system_state_t;

联合体(union)更是省RAM的利器。多个变量共享同一块内存,这在处理协议解析、寄存器映射时特别有用。

typedef union {
    uint32_t value;
    struct {
        uint8_t byte0;
        uint8_t byte1;
        uint8_t byte2;
        uint8_t byte3;
    } bytes;
} reg32_t;

reg32_t reg;
reg.value = 0x12345678;
// 现在 reg.bytes.byte0 = 0x78 (小端模式)

个人经验:我在做I2C传感器驱动时,经常用联合体来解析寄存器数据。一个联合体搞定所有字节序问题,代码简洁又高效。

3.4 位域的精打细算

位域(bit-field)是C语言里一个很特别的存在。它允许你精确控制每个bit的用途,在MCU寄存器操作中几乎是标配。

typedef struct {
    uint32_t enable    : 1;  // bit0
    uint32_t mode      : 2;  // bit1-2
    uint32_t prescaler : 4;  // bit3-6
    uint32_t reserved  : 25; // bit7-31
} timer_cr1_t;

但位域有个大坑:不同编译器对位域的布局方式不同。有的从低位开始,有的从高位开始。我建议只在同一款芯片、同一个编译器下使用位域,否则移植时你会哭的。

更稳妥的做法是:用宏定义+位运算。虽然代码看起来没那么优雅,但可移植性更好。

#define TIM_CR1_ENABLE      (1 << 0)
#define TIM_CR1_MODE_MASK   (0x3 << 1)
#define TIM_CR1_MODE_UP     (0x0 << 1)
#define TIM_CR1_MODE_DOWN   (0x1 << 1)

// 设置寄存器
TIM->CR1 = TIM_CR1_ENABLE | TIM_CR1_MODE_UP;

精打细算的原则:

  • 能用uint8_t就别用uint32_t
  • 能用位域就别用完整字节
  • 能用枚举就别用魔数
  • 能用联合体就别用多个变量

3.5 实战建议:数据类型选择表

场景 推荐类型 原因
计数器(0-255) uint8_t 省RAM,8位MCU上效率最高
传感器数据(有符号) int16_t 大多数传感器输出16位数据
时间戳(ms) uint32_t 避免溢出,32位够用
浮点运算(带FPU) float 单精度运算快,省空间
状态机 enum : uint8_t 可读性好,省空间
寄存器映射 union + struct 灵活访问位和字节

最后说一句:数据类型的选择,直接决定了你程序的性能和稳定性。别小看这一个个小小的类型定义,它们堆起来就是整个系统的根基。我在项目中见过太多因为类型选错导致的bug,有些甚至到了量产阶段才发现。嗯,希望你能从我的经验中吸取教训,少走弯路。