学习笔记 · Obsidian
C 位运算、结构体和模块边界
用位描述多个开关
一个数可以同时保存多个独立状态,例如 bit 0 表示联网、bit 1 表示数据有效、bit 2 表示报警。位编号从 0 开始。
uint32_t flags = 0u;
const uint32_t online = UINT32_C(1) << 0;
const uint32_t alarm = UINT32_C(1) << 2;
flags |= online; /* 置位 */
flags |= alarm;
flags &= ~alarm; /* 清位 */
bool connected = (flags & online) != 0u;
flags ^= online; /* 翻转 */
& 是逐位与,&& 是逻辑判断并会短路;| 和 || 也不同。写位运算条件时保留括号。这个片段假设已包含 <stdint.h>、<stdbool.h>。GNU Bitwise Operations
本例使用无符号值。动态移位前,先验证位数;这里若左操作数按 32 位计算,移位量须在 0..31。不要以为 1 << 31 和明确的无符号掩码在所有实现上等价。
位掩码不是硬件寄存器通用写法
上例改的是普通内存变量。真实寄存器可能有“写 1 清除”、只读位或保留位;未经数据手册确认就执行“读、改、写”,可能误清中断。初学先用 ESP-IDF 驱动 API,再读驱动如何访问寄存器。
这也是软件抽象的作用:调用者表达动作,驱动负责寄存器语义和访问顺序。
struct 与 enum 各负责什么
typedef enum {
DEVICE_STARTING,
DEVICE_OFFLINE,
DEVICE_ONLINE,
DEVICE_FAULT
} device_state_t;
typedef struct {
uint32_t sequence;
uint16_t reading;
bool valid;
device_state_t state;
} device_snapshot_t;
enum 给状态命名,struct 组合某一时刻的快照。GNU Structures
本课程设计采用 valid 独立表达有效性。采样失败时,不能把 reading = 0 当作错误,因为零也可能是合法读数。状态迁移则由事件决定,例如“获得 IP”不等同于“MQTT 已连接”。
结构体适合进程内数据组织。对外协议应逐字段编码,定义字节序、长度和版本;不要把 sizeof(struct) 个字节直接当网络协议。这样可以绕开填充、枚举表示和编译器差异。
static、宏和头文件
- 文件作用域的
static函数或变量,把链接可见范围限制在当前翻译单元;函数内的static对象则跨调用保留值。 #include在预处理阶段展开头文件;头文件保存声明和必要类型,实现留在.c。- 头文件用 include guard 防重复展开;不要把有外部链接的全局变量定义随意放进头文件。
#define进行文本替换。像SQUARE(i++)一类会重复求值的宏,应改成普通函数或static inline函数。
声明、存储类别和预处理的规范入口分别为 N1570 §6.7、§6.10。这里只使用与本课程模块划分相关的最小语义。
volatile 的实际边界
volatile 约束编译器对特定访问的优化。它不会自动给 count++ 加锁,也不是发布普通内存写入的同步屏障。共享状态优先用队列、互斥量或经验证的原子操作。GCC Volatiles
假设任务 A 和 B 同时执行 count++:都读到 5,再都写回 6,就丢失一次更新。给 count 加 volatile 仍无法把读取、加法和写回变成一个不可分割操作。
练习
从 flags = 0 开始,依次执行置 bit 0、置 bit 2、清 bit 0。预期结果依次为十进制 1、5、4。解释为什么 (flags & alarm) != 0 可读,而 flags && alarm 不能判断特定 bit。
将第 01 课拆成 sample.h、sample.c、main.c:类型和公开声明进头文件,函数实现进 .c。跨文件调用的 summarize 此时应去掉文件作用域 static;不要原样复制后再疑惑链接失败。