跳转至

位操作

学习目标

完成本模块后,你将能够: - 掌握C语言中的位运算符和操作技巧 - 使用位操作高效地操作硬件寄存器 - 理解位域的使用和限制 - 应用位操作优化代码性能 - 实现医疗器械软件中的位级数据处理

前置知识

  • C语言基础
  • 二进制和十六进制数制
  • 基本的逻辑运算

内容

位运算符

C语言提供了六种位运算符:

运算符 名称 示例 说明
& 按位与 a & b 两位都为1时结果为1
\| 按位或 a \| b 任一位为1时结果为1
^ 按位异或 a ^ b 两位不同时结果为1
~ 按位取反 ~a 0变1,1变0
<< 左移 a << n 左移n位,右边补0
>> 右移 a >> n 右移n位

基本示例

#include <stdint.h>

void bit_operations_basic(void) {
    uint8_t a = 0b10101100;  // 0xAC
    uint8_t b = 0b11001010;  // 0xCA

    uint8_t and_result = a & b;   // 0b10001000 = 0x88
    uint8_t or_result = a | b;    // 0b11101110 = 0xEE
    uint8_t xor_result = a ^ b;   // 0b01100110 = 0x66
    uint8_t not_result = ~a;      // 0b01010011 = 0x53

    uint8_t left_shift = a << 2;  // 0b10110000 = 0xB0
    uint8_t right_shift = a >> 2; // 0b00101011 = 0x2B
}

常用位操作技巧

1. 设置位(Set Bit)

// 设置第n位为1
#define SET_BIT(reg, bit)   ((reg) |= (1U << (bit)))

void set_bit_example(void) {
    uint32_t value = 0x00000000;

    SET_BIT(value, 3);   // value = 0x00000008
    SET_BIT(value, 7);   // value = 0x00000088
}

2. 清除位(Clear Bit)

// 清除第n位为0
#define CLEAR_BIT(reg, bit) ((reg) &= ~(1U << (bit)))

void clear_bit_example(void) {
    uint32_t value = 0xFFFFFFFF;

    CLEAR_BIT(value, 3);  // value = 0xFFFFFFF7
    CLEAR_BIT(value, 7);  // value = 0xFFFFFF77
}

3. 切换位(Toggle Bit)

// 切换第n位(0变1,1变0)
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1U << (bit)))

void toggle_bit_example(void) {
    uint32_t value = 0x00000008;

    TOGGLE_BIT(value, 3);  // value = 0x00000000
    TOGGLE_BIT(value, 3);  // value = 0x00000008
}

4. 读取位(Read Bit)

// 读取第n位的值
#define READ_BIT(reg, bit)  (((reg) >> (bit)) & 1U)

void read_bit_example(void) {
    uint32_t value = 0x00000088;

    uint8_t bit3 = READ_BIT(value, 3);  // bit3 = 1
    uint8_t bit4 = READ_BIT(value, 4);  // bit4 = 0
}

5. 修改位(Modify Bit)

// 将第n位设置为指定值
#define MODIFY_BIT(reg, bit, val) \
    ((val) ? SET_BIT(reg, bit) : CLEAR_BIT(reg, bit))

void modify_bit_example(void) {
    uint32_t value = 0x00000000;

    MODIFY_BIT(value, 3, 1);  // value = 0x00000008
    MODIFY_BIT(value, 3, 0);  // value = 0x00000000
}

多位操作

1. 设置多个位

// 设置多个位为1
#define SET_BITS(reg, mask)   ((reg) |= (mask))

void set_bits_example(void) {
    uint32_t value = 0x00000000;

    // 设置位3、4、5为1
    SET_BITS(value, 0x00000038);  // value = 0x00000038
}

2. 清除多个位

// 清除多个位为0
#define CLEAR_BITS(reg, mask) ((reg) &= ~(mask))

void clear_bits_example(void) {
    uint32_t value = 0xFFFFFFFF;

    // 清除位3、4、5为0
    CLEAR_BITS(value, 0x00000038);  // value = 0xFFFFFFC7
}

3. 读取位域

// 读取从position开始的width位
#define READ_BITS(reg, position, width) \
    (((reg) >> (position)) & ((1U << (width)) - 1))

void read_bits_example(void) {
    uint32_t value = 0x12345678;

    // 读取位4-7(4位)
    uint8_t nibble = READ_BITS(value, 4, 4);  // nibble = 0x7
}

4. 写入位域

// 将value写入从position开始的width位
#define WRITE_BITS(reg, position, width, value) \
    do { \
        uint32_t mask = ((1U << (width)) - 1) << (position); \
        (reg) = ((reg) & ~mask) | (((value) << (position)) & mask); \
    } while(0)

void write_bits_example(void) {
    uint32_t reg = 0x00000000;

    // 将0xA写入位4-7
    WRITE_BITS(reg, 4, 4, 0xA);  // reg = 0x000000A0
}

硬件寄存器操作

在嵌入式系统中,位操作最常用于配置硬件寄存器。

GPIO配置示例

#include <stdint.h>

// GPIO寄存器地址
#define GPIOA_BASE      0x40020000
#define GPIOA_MODER     (*(volatile uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_OTYPER    (*(volatile uint32_t*)(GPIOA_BASE + 0x04))
#define GPIOA_OSPEEDR   (*(volatile uint32_t*)(GPIOA_BASE + 0x08))
#define GPIOA_ODR       (*(volatile uint32_t*)(GPIOA_BASE + 0x14))
#define GPIOA_IDR       (*(volatile uint32_t*)(GPIOA_BASE + 0x10))

// 配置PA5为输出模式
void configure_gpio_output(void) {
    // 清除PA5的模式位(位10-11)
    GPIOA_MODER &= ~(0x3 << 10);

    // 设置PA5为输出模式(01)
    GPIOA_MODER |= (0x1 << 10);

    // 设置为推挽输出
    GPIOA_OTYPER &= ~(1 << 5);

    // 设置为高速
    GPIOA_OSPEEDR |= (0x3 << 10);
}

// 设置PA5输出高电平
void set_gpio_high(void) {
    GPIOA_ODR |= (1 << 5);
}

// 设置PA5输出低电平
void set_gpio_low(void) {
    GPIOA_ODR &= ~(1 << 5);
}

// 切换PA5输出状态
void toggle_gpio(void) {
    GPIOA_ODR ^= (1 << 5);
}

// 读取PA3输入状态
uint8_t read_gpio_input(void) {
    return (GPIOA_IDR >> 3) & 0x01;
}

定时器配置示例

// 定时器寄存器
#define TIM2_BASE       0x40000000
#define TIM2_CR1        (*(volatile uint32_t*)(TIM2_BASE + 0x00))
#define TIM2_DIER       (*(volatile uint32_t*)(TIM2_BASE + 0x0C))
#define TIM2_SR         (*(volatile uint32_t*)(TIM2_BASE + 0x10))
#define TIM2_CNT        (*(volatile uint32_t*)(TIM2_BASE + 0x24))
#define TIM2_PSC        (*(volatile uint32_t*)(TIM2_BASE + 0x28))
#define TIM2_ARR        (*(volatile uint32_t*)(TIM2_BASE + 0x2C))

// CR1寄存器位定义
#define TIM_CR1_CEN     (1 << 0)   // 计数器使能
#define TIM_CR1_UDIS    (1 << 1)   // 更新禁止
#define TIM_CR1_URS     (1 << 2)   // 更新请求源
#define TIM_CR1_OPM     (1 << 3)   // 单脉冲模式
#define TIM_CR1_ARPE    (1 << 7)   // 自动重载预装载使能

// 配置定时器
void configure_timer(void) {
    // 禁用定时器
    TIM2_CR1 &= ~TIM_CR1_CEN;

    // 设置预分频器
    TIM2_PSC = 7999;  // 8MHz / (7999+1) = 1kHz

    // 设置自动重载值
    TIM2_ARR = 999;   // 1kHz / (999+1) = 1Hz

    // 使能更新中断
    TIM2_DIER |= (1 << 0);

    // 使能自动重载预装载
    TIM2_CR1 |= TIM_CR1_ARPE;

    // 启动定时器
    TIM2_CR1 |= TIM_CR1_CEN;
}

// 清除更新中断标志
void clear_timer_interrupt(void) {
    TIM2_SR &= ~(1 << 0);
}

位域(Bit Fields)

位域允许在结构体中定义位级成员。

基本语法

typedef struct {
    uint32_t bit0 : 1;      // 1位
    uint32_t bit1_3 : 3;    // 3位
    uint32_t bit4_7 : 4;    // 4位
    uint32_t reserved : 24; // 24位保留
} bitfield_t;

void bitfield_example(void) {
    bitfield_t bf;

    bf.bit0 = 1;
    bf.bit1_3 = 0x5;
    bf.bit4_7 = 0xA;
}

寄存器映射示例

typedef struct {
    uint32_t CEN : 1;       // 位0:计数器使能
    uint32_t UDIS : 1;      // 位1:更新禁止
    uint32_t URS : 1;       // 位2:更新请求源
    uint32_t OPM : 1;       // 位3:单脉冲模式
    uint32_t DIR : 1;       // 位4:方向
    uint32_t CMS : 2;       // 位5-6:中心对齐模式
    uint32_t ARPE : 1;      // 位7:自动重载预装载
    uint32_t CKD : 2;       // 位8-9:时钟分频
    uint32_t reserved : 22; // 位10-31:保留
} TIM_CR1_Bits;

typedef union {
    uint32_t value;
    TIM_CR1_Bits bits;
} TIM_CR1_Register;

void bitfield_register_example(void) {
    TIM_CR1_Register cr1;

    // 通过位域访问
    cr1.bits.CEN = 1;
    cr1.bits.ARPE = 1;

    // 通过整体值访问
    uint32_t reg_value = cr1.value;
}

位域的限制

  • 位域的内存布局依赖于编译器实现
  • 不同编译器可能产生不同的结果
  • 在医疗器械软件中,建议使用显式的位操作而非位域
  • MISRA C规则限制位域的使用

实用位操作技巧

1. 检查数字是否为2的幂

bool is_power_of_two(uint32_t n) {
    return (n != 0) && ((n & (n - 1)) == 0);
}

说明: 这是判断一个数是否为2的幂的高效算法。原理是2的幂的二进制表示只有一个1,n-1会将这个1变为0,其后的0变为1,两者按位与的结果为0。例如:8(1000) & 7(0111) = 0。

2. 计算设置位的数量(Population Count)

uint32_t count_set_bits(uint32_t n) {
    uint32_t count = 0;
    while (n) {
        count += n & 1;
        n >>= 1;
    }
    return count;
}

// 更高效的方法(Brian Kernighan算法)
uint32_t count_set_bits_fast(uint32_t n) {
    uint32_t count = 0;
    while (n) {
        n &= (n - 1);  // 清除最低位的1
        count++;
    }
    return count;
}

3. 反转位

uint8_t reverse_bits(uint8_t n) {
    uint8_t result = 0;
    for (int i = 0; i < 8; i++) {
        result <<= 1;
        result |= (n & 1);
        n >>= 1;
    }
    return result;
}

说明: 这是反转字节中所有位的算法。通过循环8次,每次将result左移1位,然后将n的最低位加到result,最后将n右移1位。这样就将n的位从低到高依次移到result的高到低位置。

4. 交换两个变量(不使用临时变量)

void swap_without_temp(uint32_t* a, uint32_t* b) {
    if (a != b) {  // 防止同一地址
        *a ^= *b;
        *b ^= *a;
        *a ^= *b;
    }
}

5. 获取最低位的1

uint32_t get_lowest_set_bit(uint32_t n) {
    return n & (-n);
}

说明: 这是获取最低置位位的算法。-n是n的补码,等于~n+1。n与-n按位与的结果只保留最低的1位。例如:12(1100) & -12(0100) = 4(0100),得到最低的置位位。

医疗器械软件中的位操作最佳实践

  1. 使用宏定义提高可读性

    // 定义位掩码
    #define STATUS_READY    (1 << 0)
    #define STATUS_ERROR    (1 << 1)
    #define STATUS_BUSY     (1 << 2)
    
    // 使用有意义的名称
    void check_status(uint32_t status) {
        if (status & STATUS_ERROR) {
            handle_error();
        }
    }
    

  2. 使用类型安全的宏

    #define SET_BIT(reg, bit)   ((reg) |= (1U << (bit)))
    // 使用U后缀确保无符号运算
    

  3. 避免有符号数的右移

    // 错误:有符号数右移可能是算术右移
    int32_t value = -8;
    int32_t result = value >> 2;  // 结果依赖于实现
    
    // 正确:使用无符号数
    uint32_t value = 0xFFFFFFF8;
    uint32_t result = value >> 2;  // 逻辑右移
    

  4. 使用volatile访问硬件寄存器

    #define REG (*(volatile uint32_t*)0x40000000)
    
    void access_register(void) {
        REG |= (1 << 5);  // volatile确保不被优化
    }
    

  5. 遵循MISRA C规则

  6. Rule 10.1: 不要对有符号数进行位运算
  7. Rule 12.7: 位运算符不应用于有符号操作数
  8. Rule 12.9: 右移运算符不应用于有符号操作数

实践示例:状态寄存器管理

#include <stdint.h>
#include <stdbool.h>

// 设备状态位定义
#define DEV_STATUS_POWER_ON     (1U << 0)
#define DEV_STATUS_INITIALIZED  (1U << 1)
#define DEV_STATUS_MEASURING    (1U << 2)
#define DEV_STATUS_DATA_READY   (1U << 3)
#define DEV_STATUS_ERROR        (1U << 4)
#define DEV_STATUS_CALIBRATED   (1U << 5)

typedef struct {
    volatile uint32_t status;
    volatile uint32_t control;
    volatile uint32_t data;
} device_registers_t;

static device_registers_t* const device = (device_registers_t*)0x40010000;

// 设置状态位
void device_set_status(uint32_t status_bits) {
    device->status |= status_bits;
}

// 清除状态位
void device_clear_status(uint32_t status_bits) {
    device->status &= ~status_bits;
}

// 检查状态位
bool device_check_status(uint32_t status_bits) {
    return (device->status & status_bits) == status_bits;
}

// 等待状态位
bool device_wait_for_status(uint32_t status_bits, uint32_t timeout_ms) {
    uint32_t start_time = get_tick_count();

    while (!device_check_status(status_bits)) {
        if ((get_tick_count() - start_time) > timeout_ms) {
            return false;  // 超时
        }
    }

    return true;
}

// 使用示例
void device_operation_example(void) {
    // 上电
    device_set_status(DEV_STATUS_POWER_ON);

    // 初始化
    if (device_initialize()) {
        device_set_status(DEV_STATUS_INITIALIZED);
    }

    // 开始测量
    device_set_status(DEV_STATUS_MEASURING);

    // 等待数据就绪
    if (device_wait_for_status(DEV_STATUS_DATA_READY, 1000)) {
        uint32_t data = device->data;
        process_measurement(data);
        device_clear_status(DEV_STATUS_DATA_READY);
    }

    // 停止测量
    device_clear_status(DEV_STATUS_MEASURING);
}

实践练习

  1. 实现一个位图(bitmap)数据结构,用于管理资源分配
  2. 编写函数将32位整数的字节序从大端转换为小端
  3. 实现CRC校验算法(使用位操作)
  4. 配置一个实际的硬件外设(如SPI或I2C)

相关资源

相关知识模块

深入学习

参考文献

  1. MISRA C:2012 - Guidelines for the use of the C language in critical systems
  2. "Hacker's Delight" by Henry S. Warren Jr.
  3. ARM Cortex-M3/M4 Technical Reference Manual
  4. "Embedded C Coding Standard" by Michael Barr
  5. IEC 62304:2006+AMD1:2015 - Medical device software

自测问题

问题1:位操作的基本运算符有哪些?

问题:列举C语言中的位操作运算符,并说明每个运算符的作用。

答案

位操作运算符

  1. 按位与(&)
  2. 两个位都为1时结果为1
  3. 用途:清除特定位、提取特定位
  4. 示例:value & 0x0F 提取低4位

  5. 按位或(|)

  6. 任一位为1时结果为1
  7. 用途:设置特定位
  8. 示例:value | 0x80 设置最高位

  9. 按位异或(^)

  10. 两个位不同时结果为1
  11. 用途:翻转特定位、简单加密
  12. 示例:value ^ 0xFF 翻转所有位

  13. 按位取反(~)

  14. 将0变1,1变0
  15. 用途:生成掩码、位翻转
  16. 示例:~0x0F 生成高位掩码

  17. 左移(<<)

  18. 向左移动位,右侧补0
  19. 用途:乘以2的幂、构造位掩码
  20. 示例:1 << 3 等于8

  21. 右移(>>)

  22. 向右移动位
  23. 用途:除以2的幂、提取高位
  24. 示例:value >> 4 右移4位

知识点回顾:位操作是嵌入式编程的基础,用于高效的硬件控制和数据处理。

问题2:如何设置、清除和翻转特定位?

问题:编写代码实现设置、清除和翻转一个字节中的特定位。

答案

位操作基本模式

#include <stdint.h>

// 设置特定位(置1)
void set_bit(uint8_t* value, uint8_t bit_position) {
    *value |= (1 << bit_position);
}

// 清除特定位(置0)
void clear_bit(uint8_t* value, uint8_t bit_position) {
    *value &= ~(1 << bit_position);
}

// 翻转特定位
void toggle_bit(uint8_t* value, uint8_t bit_position) {
    *value ^= (1 << bit_position);
}

// 测试特定位
bool test_bit(uint8_t value, uint8_t bit_position) {
    return (value & (1 << bit_position)) != 0;
}

// 使用示例
uint8_t reg = 0x00;
set_bit(&reg, 3);      // reg = 0x08 (0b00001000)
set_bit(&reg, 7);      // reg = 0x88 (0b10001000)
clear_bit(&reg, 3);    // reg = 0x80 (0b10000000)
toggle_bit(&reg, 7);   // reg = 0x00 (0b00000000)

宏定义版本

#define SET_BIT(value, bit)    ((value) |= (1 << (bit)))
#define CLEAR_BIT(value, bit)  ((value) &= ~(1 << (bit)))
#define TOGGLE_BIT(value, bit) ((value) ^= (1 << (bit)))
#define TEST_BIT(value, bit)   (((value) & (1 << (bit))) != 0)

知识点回顾:这些是最常用的位操作模式,应该熟练掌握。

问题3:如何提取和设置多个连续位?

问题:如何从一个32位整数中提取或设置连续的多个位(位域)?

答案

位域操作

#include <stdint.h>

// 提取位域
// value: 源值
// start: 起始位位置(从0开始)
// length: 位域长度
uint32_t extract_bits(uint32_t value, uint8_t start, uint8_t length) {
    // 创建掩码:length个1
    uint32_t mask = (1U << length) - 1;
    // 右移到起始位置,然后应用掩码
    return (value >> start) & mask;
}

// 设置位域
uint32_t set_bits(uint32_t value, uint8_t start, uint8_t length, 
                 uint32_t new_bits) {
    // 创建掩码
    uint32_t mask = ((1U << length) - 1) << start;
    // 清除目标位域
    value &= ~mask;
    // 设置新值
    value |= (new_bits << start) & mask;
    return value;
}

// 使用示例
uint32_t reg = 0x12345678;

// 提取位12-15(4位)
uint32_t field = extract_bits(reg, 12, 4);  // 提取0x5

// 设置位8-11为0xA
reg = set_bits(reg, 8, 4, 0xA);  // reg = 0x12345A78

使用位域结构体

typedef struct {
    uint32_t field1 : 4;   // 位0-3
    uint32_t field2 : 8;   // 位4-11
    uint32_t field3 : 12;  // 位12-23
    uint32_t field4 : 8;   // 位24-31
} register_t;

register_t reg;
reg.field2 = 0xAB;  // 直接访问位域

知识点回顾:位域操作常用于寄存器配置和数据打包。

问题4:位操作在硬件寄存器配置中的应用

问题:如何使用位操作安全地配置硬件寄存器,避免影响其他位?

答案

安全的寄存器配置模式

#include <stdint.h>

// 定义寄存器地址
#define GPIO_CTRL_REG  (*(volatile uint32_t*)0x40020000)

// 定义位掩码
#define GPIO_MODE_MASK    0x03  // 位0-1
#define GPIO_SPEED_MASK   0x0C  // 位2-3
#define GPIO_PULL_MASK    0x30  // 位4-5

// 定义位位置
#define GPIO_MODE_POS     0
#define GPIO_SPEED_POS    2
#define GPIO_PULL_POS     4

// 安全配置函数
void configure_gpio_mode(uint8_t mode) {
    uint32_t temp = GPIO_CTRL_REG;
    // 清除模式位
    temp &= ~(GPIO_MODE_MASK << GPIO_MODE_POS);
    // 设置新模式
    temp |= (mode & GPIO_MODE_MASK) << GPIO_MODE_POS;
    // 写回寄存器
    GPIO_CTRL_REG = temp;
}

// 读-修改-写模式
void set_gpio_speed(uint8_t speed) {
    GPIO_CTRL_REG = (GPIO_CTRL_REG & ~(GPIO_SPEED_MASK << GPIO_SPEED_POS))
                  | ((speed & GPIO_SPEED_MASK) << GPIO_SPEED_POS);
}

// 原子操作(如果硬件支持)
void atomic_set_bit(volatile uint32_t* reg, uint8_t bit) {
    // 使用硬件的位设置寄存器
    *(reg + 0x18/4) = (1 << bit);  // BSRR寄存器偏移
}

最佳实践: 1. 使用读-修改-写模式 2. 使用掩码保护其他位 3. 使用volatile关键字 4. 考虑中断安全性 5. 使用硬件原子操作(如果可用)

知识点回顾:正确的寄存器配置对嵌入式系统的稳定性至关重要。

问题5:位操作的常见陷阱和注意事项

问题:在使用位操作时,有哪些常见的错误和需要注意的地方?

答案

常见陷阱

1. 运算符优先级错误

// 错误:& 优先级低于 ==
if (value & 0x80 == 0x80)  // 错误!

// 正确:使用括号
if ((value & 0x80) == 0x80)  // 正确

2. 有符号数右移

int8_t value = -1;  // 0xFF
value >> 1;  // 可能是0xFF(算术右移)或0x7F(逻辑右移)

// 使用无符号类型
uint8_t value = 0xFF;
value >> 1;  // 确定是0x7F

3. 移位溢出

uint8_t value = 1;
value << 8;   // 未定义行为!超出类型范围

// 正确:使用更大的类型
uint16_t result = (uint16_t)value << 8;

4. 宏定义的副作用

// 危险的宏
#define SET_BIT(v, b) v |= (1 << b)
SET_BIT(get_value(), 3);  // get_value()被调用两次!

// 安全的宏
#define SET_BIT(v, b) ((v) |= (1 << (b)))

5. volatile关键字缺失

// 错误:编译器可能优化掉
uint32_t* reg = (uint32_t*)0x40000000;
*reg = 0x01;

// 正确:使用volatile
volatile uint32_t* reg = (volatile uint32_t*)0x40000000;
*reg = 0x01;

注意事项: - 始终使用括号明确优先级 - 对硬件寄存器使用volatile - 使用无符号类型进行位操作 - 注意移位范围 - 使用MISRA C规则检查

知识点回顾:避免这些陷阱可以防止难以调试的错误。


💬 讨论区

欢迎在这里分享您的想法、提出问题或参与讨论。需要 GitHub 账号登录。