C 语言核心能力¶
概述¶
C 语言是 Zephyr RTOS 开发的基础。本章节将帮助你掌握嵌入式系统开发中最关键的 C 语言特性,包括指针操作、结构体设计、位操作技巧以及嵌入式 C 编程的特殊关键字。
学习目标
- 深入理解指针的使用和常见陷阱
- 掌握结构体在嵌入式系统中的应用
- 熟练运用位操作进行硬件寄存器控制
- 理解 volatile、const、static 在嵌入式开发中的作用
- 能够阅读和编写 Zephyr 风格的 C 代码
1. 指针深入理解¶
指针是 C 语言最强大也最容易出错的特性。在 Zephyr 开发中,指针无处不在:设备驱动、内存管理、回调函数等。
1.1 指针基础操作¶
指针声明、取地址和解引用:
#include <zephyr/kernel.h>
void pointer_basics(void)
{
int value = 42;
int *ptr; // 声明指针
ptr = &value; // 取地址:ptr 指向 value
printk("value = %d\n", value); // 输出: 42
printk("&value = %p\n", &value); // 输出: value 的地址
printk("ptr = %p\n", ptr); // 输出: ptr 存储的地址(与 &value 相同)
printk("*ptr = %d\n", *ptr); // 解引用:输出 42
*ptr = 100; // 通过指针修改 value
printk("value = %d\n", value); // 输出: 100
}
1.2 指针与数组¶
在 C 语言中,数组名本质上是指向数组首元素的指针:
#include <zephyr/kernel.h>
void array_pointer_relationship(void)
{
uint8_t buffer[5] = {1, 2, 3, 4, 5};
uint8_t *ptr = buffer; // 数组名即指针
// 以下三种访问方式等价
printk("buffer[2] = %d\n", buffer[2]); // 数组下标访问
printk("*(buffer + 2) = %d\n", *(buffer + 2)); // 指针算术
printk("ptr[2] = %d\n", ptr[2]); // 指针下标访问
// 指针遍历数组
for (int i = 0; i < 5; i++) {
printk("*(ptr + %d) = %d\n", i, *(ptr + i));
}
}
1.3 函数指针(Zephyr 回调函数)¶
Zephyr 中大量使用函数指针实现回调机制,例如定时器回调、中断处理等:
#include <zephyr/kernel.h>
// 定义定时器回调函数类型
typedef void (*timer_callback_t)(struct k_timer *timer);
// 定时器回调函数实现
void my_timer_callback(struct k_timer *timer)
{
printk("Timer expired!\n");
}
void function_pointer_example(void)
{
// 声明并初始化定时器
K_TIMER_DEFINE(my_timer, my_timer_callback, NULL);
// 启动定时器:1 秒后触发,周期 1 秒
k_timer_start(&my_timer, K_SECONDS(1), K_SECONDS(1));
}
Zephyr 设备驱动中的函数指针:
#include <zephyr/device.h>
// GPIO 驱动 API 结构体(包含函数指针)
struct gpio_driver_api {
int (*pin_configure)(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags);
int (*port_get_raw)(const struct device *dev, gpio_port_value_t *value);
int (*port_set_masked_raw)(const struct device *dev, gpio_port_pins_t mask,
gpio_port_value_t value);
// ... 更多函数指针
};
// 使用驱动 API
void use_gpio_driver(const struct device *gpio_dev)
{
const struct gpio_driver_api *api =
(const struct gpio_driver_api *)gpio_dev->api;
// 通过函数指针调用驱动函数
api->pin_configure(gpio_dev, 13, GPIO_OUTPUT);
}
1.4 常见指针陷阱¶
野指针(Dangling Pointer)
野指针是指向已释放或无效内存的指针,使用野指针会导致未定义行为。
悬空指针(Dangling Pointer)
指向已释放内存的指针:
指针越界
访问数组边界之外的内存:
2. 结构体与联合体¶
结构体是 Zephyr 中组织数据的核心方式,从设备描述符到内核对象,都使用结构体。
2.1 结构体定义和初始化¶
基本结构体定义:
#include <zephyr/kernel.h>
// 定义传感器数据结构体
struct sensor_data {
int16_t temperature; // 温度(0.01°C 单位)
uint16_t humidity; // 湿度(0.01% 单位)
uint32_t timestamp; // 时间戳(毫秒)
};
void struct_initialization(void)
{
// 方法 1:逐字段初始化
struct sensor_data data1;
data1.temperature = 2550; // 25.50°C
data1.humidity = 6500; // 65.00%
data1.timestamp = k_uptime_get_32();
// 方法 2:初始化列表(推荐)
struct sensor_data data2 = {
.temperature = 2550,
.humidity = 6500,
.timestamp = k_uptime_get_32()
};
printk("Temperature: %d.%02d°C\n",
data2.temperature / 100, data2.temperature % 100);
}
Zephyr 设备结构体示例:
#include <zephyr/device.h>
// Zephyr 设备结构体(简化版)
struct device {
const char *name; // 设备名称
const void *config; // 配置数据
const void *api; // API 函数指针表
void *data; // 运行时数据
uint8_t state; // 设备状态
};
// 自定义 I2C 传感器设备数据
struct my_sensor_data {
const struct device *i2c_dev; // I2C 总线设备
uint8_t i2c_addr; // I2C 地址
int16_t last_reading; // 最后读数
};
// 设备配置
struct my_sensor_config {
const char *i2c_bus_label;
uint8_t i2c_address;
};
// 使用 Zephyr 宏定义设备
#define MY_SENSOR_INIT(inst) \
static struct my_sensor_data my_sensor_data_##inst = { \
.i2c_addr = DT_INST_REG_ADDR(inst), \
}; \
static const struct my_sensor_config my_sensor_config_##inst = { \
.i2c_bus_label = DT_INST_BUS_LABEL(inst), \
.i2c_address = DT_INST_REG_ADDR(inst), \
};
2.2 位域(硬件寄存器操作)¶
位域允许在结构体中定义按位存储的字段,非常适合映射硬件寄存器:
#include <stdint.h>
// 定义 GPIO 控制寄存器(假设 32 位)
struct gpio_ctrl_reg {
uint32_t pin_mode : 2; // 位 0-1: 引脚模式(输入/输出/复用)
uint32_t pull_type : 2; // 位 2-3: 上拉/下拉配置
uint32_t speed : 2; // 位 4-5: 输出速度
uint32_t reserved1 : 2; // 位 6-7: 保留
uint32_t output_type : 1; // 位 8: 输出类型(推挽/开漏)
uint32_t reserved2 : 23; // 位 9-31: 保留
};
void configure_gpio_pin(volatile struct gpio_ctrl_reg *reg)
{
// 配置为输出模式,推挽输出,高速
reg->pin_mode = 0b01; // 输出模式
reg->output_type = 0; // 推挽输出
reg->speed = 0b11; // 高速
reg->pull_type = 0b00; // 无上下拉
}
位域使用注意事项
- 位域的内存布局依赖于编译器实现,不同编译器可能不同
- 位域不能取地址(不能使用
&运算符) - 跨平台代码中,建议使用位操作宏而非位域
- Zephyr 更推荐使用
BIT()宏和位操作函数
2.3 结构体对齐和填充¶
编译器会自动对齐结构体成员以提高访问效率,这可能导致结构体大小大于所有成员大小之和:
#include <stdio.h>
// 未优化的结构体
struct unoptimized {
char a; // 1 字节
int b; // 4 字节(对齐到 4 字节边界,前面填充 3 字节)
char c; // 1 字节
int d; // 4 字节(对齐到 4 字节边界,前面填充 3 字节)
}; // 总大小:16 字节(1 + 3填充 + 4 + 1 + 3填充 + 4)
// 优化后的结构体(重新排列成员)
struct optimized {
int b; // 4 字节
int d; // 4 字节
char a; // 1 字节
char c; // 1 字节
}; // 总大小:12 字节(4 + 4 + 1 + 1 + 2填充)
void struct_alignment_demo(void)
{
printk("sizeof(unoptimized) = %zu\n", sizeof(struct unoptimized)); // 16
printk("sizeof(optimized) = %zu\n", sizeof(struct optimized)); // 12
}
结构体优化建议
- 将大尺寸成员(如
int、long、指针)放在前面 - 将小尺寸成员(如
char、bool)放在后面 - 使用
__packed属性强制紧凑排列(但会降低访问效率) - 在嵌入式系统中,内存优化很重要,但不要过度优化影响可读性
3. 位操作¶
位操作是嵌入式开发的基本功,用于控制硬件寄存器、设置标志位等。
3.1 基本位操作¶
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
void basic_bit_operations(void)
{
uint32_t reg = 0x00000000;
// 1. 设置位(Set Bit)- 将第 n 位置 1
reg |= BIT(3); // 设置第 3 位
printk("Set bit 3: 0x%08X\n", reg); // 0x00000008
// 2. 清除位(Clear Bit)- 将第 n 位置 0
reg &= ~BIT(3); // 清除第 3 位
printk("Clear bit 3: 0x%08X\n", reg); // 0x00000000
// 3. 切换位(Toggle Bit)- 翻转第 n 位
reg ^= BIT(5); // 切换第 5 位
printk("Toggle bit 5: 0x%08X\n", reg); // 0x00000020
reg ^= BIT(5); // 再次切换
printk("Toggle bit 5 again: 0x%08X\n", reg); // 0x00000000
// 4. 检查位(Test Bit)- 测试第 n 位是否为 1
reg = 0x00000028; // 二进制: 0010 1000(第 3 位和第 5 位为 1)
if (reg & BIT(3)) {
printk("Bit 3 is set\n");
}
if (!(reg & BIT(4))) {
printk("Bit 4 is clear\n");
}
}
3.2 多位操作(位段读写)¶
#include <zephyr/sys/util.h>
void multi_bit_operations(void)
{
uint32_t reg = 0x00000000;
// 1. 设置多个位
reg |= (BIT(0) | BIT(2) | BIT(4)); // 设置第 0、2、4 位
printk("Set multiple bits: 0x%08X\n", reg); // 0x00000015
// 2. 清除多个位
reg &= ~(BIT(0) | BIT(4)); // 清除第 0、4 位
printk("Clear multiple bits: 0x%08X\n", reg); // 0x00000004
// 3. 读取位段(例如读取第 4-7 位)
uint32_t value = (reg >> 4) & 0x0F; // 右移 4 位,然后屏蔽低 4 位
printk("Read bits 4-7: 0x%X\n", value);
// 4. 写入位段(例如将第 8-11 位设置为 0b1010)
reg &= ~(0x0F << 8); // 先清除第 8-11 位
reg |= (0x0A << 8); // 再设置为 0b1010
printk("Write bits 8-11: 0x%08X\n", reg); // 0x00000A04
// 5. 使用掩码操作
#define GPIO_MODE_MASK 0x03 // 位 0-1
#define GPIO_MODE_SHIFT 0
#define GPIO_SPEED_MASK 0x03 // 位 4-5
#define GPIO_SPEED_SHIFT 4
// 设置模式为 0b10(输出模式)
reg &= ~(GPIO_MODE_MASK << GPIO_MODE_SHIFT);
reg |= (0x02 << GPIO_MODE_SHIFT);
// 设置速度为 0b11(高速)
reg &= ~(GPIO_SPEED_MASK << GPIO_SPEED_SHIFT);
reg |= (0x03 << GPIO_SPEED_SHIFT);
printk("Configure GPIO: 0x%08X\n", reg);
}
3.3 Zephyr 中的 BIT() 宏¶
Zephyr 提供了一系列位操作宏,使代码更清晰:
#include <zephyr/sys/util.h>
// BIT(n) - 生成第 n 位为 1 的值
#define MY_FLAG_ENABLE BIT(0) // 0x00000001
#define MY_FLAG_READY BIT(1) // 0x00000002
#define MY_FLAG_ERROR BIT(2) // 0x00000004
void zephyr_bit_macros(void)
{
uint32_t flags = 0;
// 设置标志
flags |= MY_FLAG_ENABLE;
flags |= MY_FLAG_READY;
// 检查标志
if (flags & MY_FLAG_ENABLE) {
printk("Device is enabled\n");
}
// 清除标志
flags &= ~MY_FLAG_READY;
// GENMASK(h, l) - 生成从第 l 位到第 h 位都为 1 的掩码
uint32_t mask = GENMASK(7, 4); // 0x000000F0(第 4-7 位为 1)
printk("GENMASK(7, 4) = 0x%08X\n", mask);
// BIT_MASK(n) - 生成低 n 位都为 1 的掩码
uint32_t mask2 = BIT_MASK(4); // 0x0000000F(低 4 位为 1)
printk("BIT_MASK(4) = 0x%08X\n", mask2);
}
位操作最佳实践
- 使用
BIT(n)宏而不是(1 << n),更清晰 - 使用有意义的宏名称定义位标志,提高可读性
- 操作硬件寄存器时,先读取、修改、再写回(Read-Modify-Write)
- 注意位操作的优先级,必要时使用括号
4. 嵌入式 C 特性¶
嵌入式 C 编程有一些特殊的关键字和用法,理解它们对于编写正确的 Zephyr 代码至关重要。
4.1 volatile 关键字¶
volatile 告诉编译器该变量可能被程序外部因素改变(如硬件、中断),不要对其进行优化。
使用场景 1:硬件寄存器
#include <stdint.h>
// 定义 GPIO 寄存器地址(假设)
#define GPIO_BASE_ADDR 0x40020000
#define GPIO_IDR_OFFSET 0x10
// 错误:不使用 volatile
uint32_t *gpio_idr = (uint32_t *)(GPIO_BASE_ADDR + GPIO_IDR_OFFSET);
void read_gpio_wrong(void)
{
uint32_t value1 = *gpio_idr;
uint32_t value2 = *gpio_idr;
// 编译器可能优化为只读取一次,value1 和 value2 使用相同的值
}
// 正确:使用 volatile
volatile uint32_t *gpio_idr_v = (volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_IDR_OFFSET);
void read_gpio_correct(void)
{
uint32_t value1 = *gpio_idr_v;
uint32_t value2 = *gpio_idr_v;
// 编译器保证每次都从硬件读取,value1 和 value2 可能不同
}
使用场景 2:中断服务程序中修改的变量
#include <zephyr/kernel.h>
// 错误:不使用 volatile
static int button_pressed = 0;
void button_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
button_pressed = 1; // 在 ISR 中修改
}
void main_loop_wrong(void)
{
while (!button_pressed) {
// 编译器可能优化为死循环,因为它认为 button_pressed 不会改变
k_sleep(K_MSEC(100));
}
}
// 正确:使用 volatile
static volatile int button_pressed_v = 0;
void button_isr_correct(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
button_pressed_v = 1;
}
void main_loop_correct(void)
{
while (!button_pressed_v) {
// 编译器保证每次都重新读取 button_pressed_v
k_sleep(K_MSEC(100));
}
}
volatile 不保证原子性
volatile 只保证每次访问都从内存读取,不保证操作的原子性。对于多线程共享变量,需要使用互斥锁或原子操作。
4.2 const 关键字¶
const 声明常量,防止意外修改,也可以帮助编译器优化。
使用场景 1:常量配置
#include <zephyr/device.h>
// 设备配置数据
struct sensor_config {
const char *i2c_bus_label; // 指向常量字符串的指针
uint8_t i2c_address;
uint16_t sample_rate;
};
// 定义常量配置(整个结构体实例是 const)
static const struct sensor_config my_sensor_cfg = {
.i2c_bus_label = "I2C_1",
.i2c_address = 0x48,
.sample_rate = 100
};
void use_config(void)
{
// 可以读取
printk("I2C address: 0x%02X\n", my_sensor_cfg.i2c_address);
// 编译错误:不能修改 const 结构体实例的成员
// my_sensor_cfg.i2c_address = 0x49;
// 也不能修改指针指向的字符串
// my_sensor_cfg.i2c_bus_label[0] = 'X'; // 编译错误
}
使用场景 2:只读数据(节省 RAM)
// 存储在 Flash(ROM)中,不占用 RAM
static const uint8_t sine_table[256] = {
128, 131, 134, 137, /* ... */ 125
};
// 存储在 RAM 中
static uint8_t buffer[256];
void const_data_placement(void)
{
// sine_table 在 Flash 中,只读
uint8_t value = sine_table[100];
// buffer 在 RAM 中,可读写
buffer[100] = value;
}
const 指针的四种形式:
int value = 42;
// 1. 指向常量的指针(指针可变,指向的数据不可变)
const int *ptr1 = &value;
// ptr1 = &other_value; // OK
// *ptr1 = 100; // 错误:不能修改指向的数据
// 2. 常量指针(指针不可变,指向的数据可变)
int *const ptr2 = &value;
// ptr2 = &other_value; // 错误:不能修改指针
// *ptr2 = 100; // OK
// 3. 指向常量的常量指针(指针和数据都不可变)
const int *const ptr3 = &value;
// ptr3 = &other_value; // 错误
// *ptr3 = 100; // 错误
// 4. 普通指针(指针和数据都可变)
int *ptr4 = &value;
// ptr4 = &other_value; // OK
// *ptr4 = 100; // OK
4.3 static 关键字¶
static 有两个主要用途:限制作用域和保持变量生命周期。
使用场景 1:文件作用域(限制符号可见性)
// my_driver.c
// 静态全局变量:只在本文件可见
static int driver_initialized = 0;
// 静态函数:只在本文件可见
static int internal_helper(void)
{
return 42;
}
// 公共函数:其他文件可见
int my_driver_init(void)
{
if (!driver_initialized) {
internal_helper();
driver_initialized = 1;
}
return 0;
}
使用场景 2:函数内静态变量(保持状态)
#include <zephyr/kernel.h>
// 计数器函数:每次调用返回递增的值
int get_next_id(void)
{
static int counter = 0; // 静态局部变量,只初始化一次
return counter++;
}
void static_variable_demo(void)
{
printk("ID 1: %d\n", get_next_id()); // 输出: 0
printk("ID 2: %d\n", get_next_id()); // 输出: 1
printk("ID 3: %d\n", get_next_id()); // 输出: 2
}
Zephyr 中的 static 使用:
#include <zephyr/device.h>
// 驱动私有数据(static 限制作用域)
static struct my_driver_data {
const struct device *i2c_dev;
uint8_t device_addr;
bool initialized;
} driver_data;
// 驱动初始化函数(static 表示内部函数)
static int my_driver_init(const struct device *dev)
{
struct my_driver_data *data = dev->data;
// 初始化逻辑
data->initialized = true;
return 0;
}
// 设备定义(使用 static 数据)
DEVICE_DEFINE(my_device, "MY_DEVICE",
my_driver_init, NULL,
&driver_data, NULL,
POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE,
NULL);
static 使用建议
- 默认将文件内部使用的函数和变量声明为
static,减少符号冲突 - 使用
static可以帮助编译器优化(内联、死代码消除) - 在 Zephyr 驱动开发中,大量使用
static限制驱动内部实现的可见性
5. Zephyr 代码示例¶
下面是一个完整的 Zephyr 设备驱动示例,综合运用了本章所学的 C 语言特性:
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/sys/util.h>
// 设备寄存器定义(使用位操作宏)
#define REG_CTRL 0x00
#define REG_STATUS 0x01
#define REG_DATA_H 0x02
#define REG_DATA_L 0x03
// 控制寄存器位定义
#define CTRL_ENABLE BIT(0)
#define CTRL_RESET BIT(1)
#define CTRL_MODE_MASK GENMASK(3, 2)
#define CTRL_MODE_SHIFT 2
// 状态寄存器位定义
#define STATUS_READY BIT(0)
#define STATUS_ERROR BIT(7)
// 设备配置结构体(const,存储在 Flash)
struct my_sensor_config {
const char *i2c_bus_label;
uint8_t i2c_address;
};
// 设备运行时数据结构体
struct my_sensor_data {
const struct device *i2c_dev;
int16_t last_reading;
volatile bool data_ready; // 可能在 ISR 中修改
};
// 静态辅助函数:读取寄存器
static int read_register(const struct device *dev, uint8_t reg, uint8_t *value)
{
const struct my_sensor_config *cfg = dev->config;
struct my_sensor_data *data = dev->data;
return i2c_reg_read_byte(data->i2c_dev, cfg->i2c_address, reg, value);
}
// 静态辅助函数:写入寄存器
static int write_register(const struct device *dev, uint8_t reg, uint8_t value)
{
const struct my_sensor_config *cfg = dev->config;
struct my_sensor_data *data = dev->data;
return i2c_reg_write_byte(data->i2c_dev, cfg->i2c_address, reg, value);
}
// 设备初始化函数
static int my_sensor_init(const struct device *dev)
{
const struct my_sensor_config *cfg = dev->config;
struct my_sensor_data *data = dev->data;
uint8_t ctrl_reg;
// 获取 I2C 总线设备
data->i2c_dev = device_get_binding(cfg->i2c_bus_label);
if (!data->i2c_dev) {
return -ENODEV;
}
// 复位设备
ctrl_reg = CTRL_RESET;
write_register(dev, REG_CTRL, ctrl_reg);
k_msleep(10);
// 配置设备:使能,设置模式为 0b10
ctrl_reg = CTRL_ENABLE;
ctrl_reg &= ~CTRL_MODE_MASK;
ctrl_reg |= (0x02 << CTRL_MODE_SHIFT);
write_register(dev, REG_CTRL, ctrl_reg);
return 0;
}
// 读取传感器数据
static int my_sensor_read(const struct device *dev, int16_t *value)
{
struct my_sensor_data *data = dev->data;
uint8_t status, data_h, data_l;
int ret;
// 检查状态寄存器
ret = read_register(dev, REG_STATUS, &status);
if (ret < 0) {
return ret;
}
// 检查数据就绪位
if (!(status & STATUS_READY)) {
return -EBUSY;
}
// 检查错误位
if (status & STATUS_ERROR) {
return -EIO;
}
// 读取数据(高字节和低字节)
read_register(dev, REG_DATA_H, &data_h);
read_register(dev, REG_DATA_L, &data_l);
// 组合为 16 位数据
*value = (int16_t)((data_h << 8) | data_l);
data->last_reading = *value;
return 0;
}
// 定义设备实例
#define MY_SENSOR_INIT(inst) \
static struct my_sensor_data my_sensor_data_##inst = { \
.data_ready = false, \
}; \
\
static const struct my_sensor_config my_sensor_config_##inst = { \
.i2c_bus_label = DT_INST_BUS_LABEL(inst), \
.i2c_address = DT_INST_REG_ADDR(inst), \
}; \
\
DEVICE_DT_INST_DEFINE(inst, \
my_sensor_init, \
NULL, \
&my_sensor_data_##inst, \
&my_sensor_config_##inst, \
POST_KERNEL, \
CONFIG_SENSOR_INIT_PRIORITY, \
NULL);
// 为设备树中的每个实例创建设备
DT_INST_FOREACH_STATUS_OKAY(MY_SENSOR_INIT)
代码说明:
- 指针使用:
dev->config、dev->data访问设备结构体成员 - const 使用:配置数据声明为
const,存储在 Flash 中节省 RAM - volatile 使用:
data_ready可能在中断中修改,使用volatile - static 使用:内部函数和数据使用
static限制作用域 - 位操作:使用
BIT()、GENMASK()宏操作寄存器位 - 结构体:使用结构体组织配置和运行时数据
6. 自我评估测试¶
测试你对 C 语言核心能力的掌握程度:
测试题 1:指针操作¶
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr + 2;
// 问题:以下表达式的值是多少?
// A. *p
// B. *(p + 1)
// C. *(p - 1)
// D. p[1]
点击查看答案
- **A. *p = 30**(p 指向 arr[2]) - **B. *(p + 1) = 40**(p + 1 指向 arr[3]) - **C. *(p - 1) = 20**(p - 1 指向 arr[1]) - **D. p[1] = 40**(等价于 *(p + 1))测试题 2:位操作¶
uint8_t reg = 0b10110100;
// 问题:执行以下操作后,reg 的值是多少?
reg &= ~BIT(2); // 清除第 2 位
reg |= BIT(0); // 设置第 0 位
reg ^= BIT(5); // 切换第 5 位
点击查看答案
1. 初始值:`0b10110100`(0xB4) 2. 清除第 2 位:`0b10110100 & ~0b00000100 = 0b10110000`(0xB0) 3. 设置第 0 位:`0b10110000 | 0b00000001 = 0b10110001`(0xB1) 4. 切换第 5 位:`0b10110001 ^ 0b00100000 = 0b10010001`(0x91) **最终答案:0b10010001(0x91,十进制 145)**测试题 3:volatile 关键字¶
volatile uint32_t *gpio_reg = (volatile uint32_t *)0x40020000;
void wait_for_ready(void)
{
while (!(*gpio_reg & BIT(0))) {
// 等待第 0 位变为 1
}
}
// 问题:为什么 gpio_reg 必须声明为 volatile?
点击查看答案
**必须使用 volatile 的原因**: 1. `gpio_reg` 指向硬件寄存器,其值可能被硬件改变 2. 如果不使用 `volatile`,编译器可能优化为: - 只读取一次 `*gpio_reg` - 如果第一次读取第 0 位为 0,则认为永远为 0 - 优化为死循环,永远不会退出 3. 使用 `volatile` 后,编译器保证每次循环都重新从硬件读取寄存器值 4. 这样当硬件将第 0 位置 1 时,循环能够正确退出 **关键点**:`volatile` 告诉编译器"这个变量可能被程序外部因素改变,不要优化对它的访问"。测试题 4:结构体对齐¶
struct example {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
// 问题:sizeof(struct example) 的值是多少?(假设 int 为 4 字节,默认对齐)
// A. 6
// B. 8
// C. 12
// D. 16
点击查看答案
**答案:C. 12 字节** **内存布局**: **总大小:12 字节** **优化建议**:重新排列成员顺序可以减少填充:测试题 5:函数指针¶
typedef void (*callback_t)(int value);
void my_callback(int value)
{
printk("Callback called with value: %d\n", value);
}
void register_callback(callback_t cb)
{
cb(42);
}
// 问题:如何正确调用 register_callback 函数?
点击查看答案
**正确调用方式**: **输出**: **说明**: - 函数名本身就是函数指针 - `my_callback` 和 `&my_callback` 在这里是等价的 - 函数指针类型必须匹配:参数类型和返回值类型都要一致7. 学习检查清单¶
完成本章学习后,你应该能够:
- [ ] 熟练使用指针进行数据访问和操作
- [ ] 理解指针与数组的关系,能够使用指针遍历数组
- [ ] 使用函数指针实现回调机制
- [ ] 识别和避免常见的指针错误(野指针、悬空指针、越界)
- [ ] 定义和使用结构体组织复杂数据
- [ ] 理解结构体对齐和填充,能够优化结构体内存布局
- [ ] 使用位域映射硬件寄存器(了解其局限性)
- [ ] 熟练使用位操作(设置、清除、切换、检查位)
- [ ] 使用 Zephyr 的
BIT()和GENMASK()宏 - [ ] 理解
volatile的作用,知道何时使用它 - [ ] 正确使用
const声明常量和只读数据 - [ ] 使用
static限制符号作用域和保持变量状态 - [ ] 能够阅读和理解 Zephyr 设备驱动代码
- [ ] 编写符合 Zephyr 编码规范的 C 代码
8. 下一步学习¶
掌握了 C 语言核心能力后,你可以继续学习:
或者直接进入:
- 第一阶段:入门筑基期:开始 Zephyr RTOS 的实战学习
9. 参考资源¶
官方文档¶
推荐阅读¶
- 《C 专家编程》(Expert C Programming)
- 《C 陷阱与缺陷》(C Traps and Pitfalls)
- 《嵌入式 C 编程》(Embedded C Coding Standard)
在线资源¶
恭喜!
你已经完成了 C 语言核心能力的学习。这些知识是 Zephyr RTOS 开发的基石,将在后续的学习中反复使用。继续保持学习的热情,向着 Zephyr 专家的目标前进!
💬 讨论与反馈
欢迎在下方评论区分享您的学习心得、提出问题或给出建议。评论系统基于 GitHub Discussions,需要 GitHub 账号登录。