嵌入式高手C

一、嵌入式C 语言整数数据类型推荐使用

  • 数值很大,超过 - 32768-32768 范围,推荐使用 long 型;
  • 在 - 32768-32768 范围内,资源有限如很大数组或者很多结构体,可以使用 short 或者 char;
  • 如果是无符号类型使用 unsigned 类型。
  • 如果数值比较小的使用 unsigned char 或者 signed char 不用 short 或者 int
  • exp:STM32 是 ARM 32 位内核,数据位宽 32 位,处理效率最高,每次访问内存,哪怕一个 byte,读写都是 32 位,因此定义 unsigned char 变量时候看内存小,实际内部会转换为 int,因此定义成 unsigned int 实际代码内存会更小。(8 位 CPU 例外)
  • 系统级与原厂代码常用的 uint8_t/uint16_t/uint32_t 均为 typedef 定义的别名,其原始类型依次是 unsigned char、unsigned short、unsigned int,用于固定数据位数以保证跨平台一致性。

二、定义和声明

  1. 高手:0 err 0 warning
  2. 习惯声明外部函数加 extern
  3. .c 负责定义实现,.h 负责声明
  4. 禁止在同一工程中多处使用相同变量名,编译器可能认为是一个变量,可以通过前缀来区别
  5. static 定义变量在函数内部会导致变量存储在静态存储区,局部变量存储在函数栈空间,这里相当于 extern

函数指针

cpp 复制代码
typedef int (*func)()
func p1,p2;
原生写法(无 typedef)
void (*fp)(int) = fun; // 定义 + 赋值
fp (123); // 间接调用
缺点:语法晦涩、复用差。
typedef 别名写法(工程推荐)
typedef void (*Fp)(int);
Fp fp = fun;
fp (123);
优点:简洁易读、适合批量 / 回调复用。

结构体挂载函数指针核心规则

  1. 结构体只存函数指针,不存函数实现;
  2. 函数实体单独写在 .c 外部;
  3. 运行时把函数地址赋值给结构体指针,完成绑定。

三、标准 结构体 + 回调 完整模板(重点)

cpp 复制代码
#include <stdio.h>
// 1.定义回调函数指针类型
typedef void (*CallBack)(int);
// 2.结构体内嵌回调指针
struct Device
{
    int data;
    CallBack cb;
};
// 3.回调函数具体实现
void myCallback(int val)
{
    printf("回调执行:%d\n", val);
}
// 4.业务函数:内部触发回调
void taskRun(struct Device *dev)
{
    if(dev->cb)
    {
        dev->cb(dev->data); 
    }
}
// 5.使用:注册回调 + 触发
int main(void)
{
    struct Device dev;
    dev.data = 666;
    dev.cb = myCallback;  // 注册回调
    taskRun(&dev);       // 内部自动调用回调
    return 0;
}

核心原理(一句话)

外部注册回调函数地址到结构体,内部业务函数通过函数指针间接反向调用,实现解耦、分层、多态(嵌入式 / 驱动通用)。

函数实现存放位置

  1. 简单项目:同文件 .c 内;
  2. 正式工程:实现放 .c,声明 / 结构体 / 指针类型放 .h。

const 有关用法

硬件平台
1. RAM:随机存取存储器

特点:可读、可写,断电数据丢失

  • 中文:运行内存
  • 单片机里:全局变量、局部变量、栈、堆 都在 RAM
2. ROM(单片机里就是 Flash):只读存储器

特点:断电不丢,一般只读,不能随便改写

  • 代码、const 常量、字库、固件 都存在 Flash(ROM)

ARM 单片机一般有:I-Cache(指令缓存)、D-Cache(数据缓存)

Cache 既不是 RAM,也不是 ROM Cache 是 CPU 内部的一小块超高速缓存比 RAM 还要快很多倍。

  1. 有 Cache:Cortex‑A / H7 高端 M 核,能优化到 Cache
  2. 无 Cache:F1/F4/M0/M3 普通单片机,最多优化到 RAM
编译器

Keil、GCC、IAR 三大嵌入式编译器全都支持

必要条件
  1. 必须开优化等级 O1/O2/Os;
  2. O0 关闭优化:所有 const 只读 Flash,不提速、不缓存。
核心

只有高频读取的 const 才会缓存;低频常量常驻 Flash。

  1. const 修饰值:值不可改,指针可改const int *p;
  2. const 修饰指针:指针不可改,值可改int *const p;
  3. 值、指针都不可改const int *const p;
  4. 普通常量(非指针)const int a;
const 在函数形参(只写声明 / 定义,无实现)
  1. 普通变量形参void fun (const int x);
  2. 指针形参(保护数据,最常用)// 不能通过指针修改内容void fun (const int *p);
  3. 字符串形参 标准写法void fun (const char *str);

四、复杂声明与 main 函数使用

  1. int *(*p [5])(int *, int *);p 是一个含 5 个元素的函数指针数组;每个指针指向:参数为两个 int 指针、返回值为 int 指针 的函数。
  2. .h 头文件:只放宏、结构体 /typedef、函数声明、extern 变量声明;不放变量定义、不放函数实现。
  3. .c 源文件:放变量定义、函数具体实现。
  4. 原因:头文件会被多个.c 重复包含,若在.h 写变量 / 函数定义,会造成重复定义报错。搭配头文件保护,就能彻底避免重复包含问题。

main 函数标准写法

  1. int main (void) ------ 标准无参,最正规
  2. int main (int argc, char **argv) ------ 标准带参,用于命令行传参
  3. int main () ------ 不严谨,弱定义无参,坑多
  4. void main (void) ------ 非标准旧语法,直接废弃

堆栈相关

  1. 所有函数内普通局部变量(含 main、自定义函数),都存储在栈区。
  2. 栈空间容量极小,系统严格限制。
  3. 函数内定义大数组、大型结构体,极易造成栈溢出,程序崩溃。
  4. 大件数据解决方案:全局变量、static 修饰、malloc 堆内存。
  5. 主流 CPU 均有硬件栈 + 专属栈指针寄存器。
  6. ARM:R13 固定为 SP(栈指针)。
  7. 汇编指令:PUSH 压栈,SP 自动偏移;POP 出栈,SP 自动复位
  8. x86:ESP 为栈指针,逻辑一致。
  9. 函数局部变量、现场保存,全靠硬件栈支撑。

堆栈配置文件

  1. .s 启动文件:startup_stm32xxxx.s
  2. 关键宏定义:Stack_Size EQU 0x400 ; 栈 默认 1KB

变量初始化

  1. 只读代码 / 常量 → Flash(code、rodata)
  2. 全局变量 → RAM(rwdata、zi)
  3. 局部变量 → 硬件栈 Stack
  4. 函数局部变量、malloc 内存,默认都是随机脏数据。
  5. memset:批量把指定内存整块清零 / 设为固定值。
  6. 常用用法:memset (内存地址,0, 长度),专门给堆内存、数组快速清 0。

字符串定义方式

  1. char *p = "ABC";字符串 ABC:存在 Flash rodata(只读)指针 p:局部在栈,全局在 RW/ZI RAM不可写!修改内容直接死机、硬件报错
  2. char p [] = "ABC";字符串拷贝到 RAM(栈 / 全局静态区)本体在内存,可读写修改占用 RAM 空间
  3. char p [] = {"ABC"};和第二种完全一样,语法写法区别,存储、属性无差异

五、结构体、联合体、枚举核心用法

结构体栈上和堆上使用场景

不需要 malloc 的场景(干净无内存问题)

  1. 特点:自动生命周期、不用手动清零、不用拷贝管理、无泄漏

    cpp 复制代码
    // 小结构体、临时用、不跨函数返回
    struct Info {
        int flag;
        short val;
    };
    void fun(void)
    {
        // 栈变量,自动回收
        struct Info info = {0}; 
        info.flag = 1;
        // 直接使用,完事函数结束自动销毁
    }

    要点:小体积、无大缓冲区;不出函数、不返回指针;零 malloc /memset/free

必须 malloc 的 4 种细化场景(全覆盖你说的点)

场景 1:结构体内含大数组,栈会炸 → 堆分配 + memset 清零
cpp 复制代码
struct BigData {
    int len;
    char buf[2048];  // 大缓冲区
};
void deal_data(const char *src_buf)
{
    // 1. 堆申请
    struct BigData *p = malloc(sizeof(struct BigData));
    
    // 2. 必须手动清零(malloc 是脏内存)
    memset(p, 0, sizeof(struct BigData));
    // 3. 数据拷贝
    p->len = strlen(src_buf);
    memcpy(p->buf, src_buf, p->len);
    // 4. 本函数临时用:用完立刻 free
    free(p);
}

规则:函数内临时堆内存 → 谁 malloc,谁当场 free

场景 2:跨函数返回结构体指针 → 内存生命周期延长

栈绝对不能返回,只能堆:

cpp 复制代码
// 提供者:申请内存
struct BigData* data_create(const char *src)
{
    struct BigData *p = malloc(sizeof(struct BigData));
    memset(p, 0, sizeof(struct BigData));
    memcpy(p->buf, src, strlen(src));
    return p; // 堆内存带出函数
}
// 使用者:负责释放
void main_func(void)
{
    struct BigData *data = data_create("hello");
    // 业务使用...
    // 关键:谁接收,谁最终 free
    free(data);
}

核心规则(工程铁律):谁申请,谁负责说明释放责任传出指针 = 移交释放权,上层用完必须 free忘了释放 → 内存泄漏,嵌入式长时间运行必崩

场景 3:全局长期持有(缓存 / 状态)→ 只申请,不频繁 free
cpp 复制代码
static struct BigData *g_cache = NULL;
void cache_init(void)
{
    g_cache = malloc(sizeof(struct BigData));
    memset(g_cache, 0, sizeof(struct BigData));
}
// 整机退出/注销时才 free
void cache_deinit(void)
{
    if(g_cache)
    {
        free(g_cache);
        g_cache = NULL;
    }
}

适用:常驻缓存、设备配置、后台常驻节点

场景 4:动态数量(链表 / 队列)→ 逐个 malloc,销毁统一 free
cpp 复制代码
struct Node {
    int data;
    struct Node *next;
};
// 新建节点
struct Node* node_new(int v)
{
    struct Node *n = malloc(sizeof(struct Node));
    memset(n, 0, sizeof(struct Node));
    n->data = v;
    return n;
}
// 销毁整条链表,统一释放
void list_free(struct Node *head)
{
    struct Node *tmp;
    while(head)
    {
        tmp = head;
        head = head->next;
        free(tmp);
    }
}

规则

  1. 栈结构体:小、临时、不出函数;不用 malloc、memset、free,= {0} 初始化即可
  2. 堆结构体(malloc)必做三件事:申请后必须 memset 清脏数据;数据搬运用 memcpy;用完必须 free
  3. 跨函数内存权责:
    • 函数内自用:本函数内 free
    • 返回指针给外部:移交释放,上层负责 free
    • 全局静态常驻:初始化申请,退出统一释放
  4. 什么时候必选堆:含大数组 / 超大结构体(防栈溢出);需要返回指针、跨生命周期使用;动态节点:链表、队列、缓存

六、展开结构体

空结构体

  1. struct x{}; sizeof(x);
  2. 标准 C 不允许空结构体;
  3. GCC 空结构体占 1 字节,只为占位、保证唯一地址;
  4. 局部空结构体变量,正常在栈上分配内存;

柔性数组占位思想

cpp 复制代码
#include <stdint.h>
#include <string.h>
// 修正后:无union,纯你要的结构
struct X
{
    uint8_t  head;    // 1字节
    uint16_t len;     // 2字节
    uint8_t  buffer[0]; // 柔性数组,不占内存
};
// 两个uint8_t 数组 大小100
uint8_t receive_buffer[100];
uint8_t data_buffer[100];
void test(void)
{
    // 数组首地址 强转为结构体指针
    struct X *p_buffer = (struct X *)receive_buffer;
    // 整段拷贝:receive_buffer → data_buffer
    memcpy(data_buffer, receive_buffer, sizeof(receive_buffer));
}

补充关键知识点

  1. 结构体本身大小(无对齐):1 + 2 = 3 字节
  2. buffer [0] 柔性数组不计入结构体大小
举例
cpp 复制代码
struct X
{
    uint8_t  head;   // 1B
    uint16_t len;    // 2B
    uint8_t  buffer[0]; // 柔性数组
};

核心第一句:柔性数组 [0] 本身不占任何 sizeof 大小,sizeof (struct X) 只算前面成员 + 结构体对齐,不含 buffer

什么是占位思想

  1. head + len 是固定协议头;
  2. buffer [0] 只是占一个位置、标记起始地址;
  3. 真正的数据载荷,紧贴在结构体头部内存的后面;
  4. 不额外分配固定空间,用多少、占多少。内存连续排布:[head (1B)] [len (2B)] [buffer 真实数据区......],buffer [0] 就指向这里,它不是提前开一块固定数组,只是标记载荷起点。

柔性数组 内存特点

  1. 不占结构体体积;
  2. 必须放在结构体最后一位;
  3. 本身无内存,真实数据靠:栈数组强转(你现在的写法)或者 malloc 一口气申请「结构体头 + 载荷总长」

数组强转结构体工程优势

优势 1:不用逐个偏移算地址,直接点成员

cpp 复制代码
// 原始字节数组
uint8_t receive_buffer[100];
// 强转直接解析协议头
struct X *p = (struct X *)receive_buffer;
p->head;   // 直接拿第1字节
p->len;    // 直接拿2~3字节
p->buffer; // 直接拿到后面载荷起始

不用手写偏移 buf [0]、buf [1]、buf [2],可读性爆炸、少 Bug。

优势 2:协议头和载荷内存连续,天然贴合串口 / CAN / 网络帧硬件收上来的原始数据流,本身就是一段连续内存,和柔性数组结构体内存布局完全匹配,天生适配。

优势 3:极度省内存如果写成固定数组:uint8_t buffer [64]; 不管数据是 5 字节还是 60 字节,永远占 64B。柔性数组:数据多长,就用多长,无冗余浪费。

优势 4:统一解包格式所有通信帧:包头 + 长度 + 载荷,全用这套柔性结构体模板,代码统一、好维护。

对比反例,为什么不用指针代替?

错误写法:

cpp 复制代码
struct Bad
{
    uint8_t head;
    uint16_t len;
    uint8_t *buf; // 指针
};

指针单独占 4/8 字节;指针是独立地址,不跟包头连续;不能直接数组强转解析;容易野指针、内存泄漏。柔性数组直接贴在头部后面,无额外开销、无指针坑。

极简笔记总结(直接抄)
  1. 柔性数组 [0] 不占用结构体大小,仅做地址占位;
  2. 核心思想:头部固定协议段 + 尾部动态变长载荷;
  3. 内存连续排布,适配原始数据流,支持数组强转直接解析;
  4. 优势:不用手动算偏移,代码简洁;动态长度、不浪费内存;无指针、无野指针、无泄漏;必须放在结构体最后一个成员。
cpp 复制代码
// 纯协议头,无柔性数组
struct X
{
    uint8_t  head;
    uint16_t len;
};
uint8_t receive_buffer[100];
uint8_t data_buffer[100];
void test(void)
{
    struct X *p_buf = (struct X *)receive_buffer;
    // 最后一个成员 len 的地址  + 1
    uint8_t *payload = (uint8_t *)(&p_buf->len + 1);
    memcpy(data_buffer, payload, p_buf->len);
}

数据选择性解析

cpp 复制代码
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// ---------------------- 两个不同业务子结构体 ----------------------
// 类型0 对应结构体
typedef struct
{
    uint16_t val1;
    uint16_t val2;
} DataA_t;
// 类型1 对应结构体
typedef struct
{
    uint32_t id;
    uint8_t  status;
} DataB_t;
// ---------------------- 带柔性数组的主结构体 ----------------------
typedef struct
{
    uint8_t  type;       // 数据类型:0=解析A,1=解析B
    uint8_t  len;        // 有效数据长度
    uint8_t  data[];     // 柔性数组
} MsgHead_t;
// ---------------------- 全局原始缓冲数组 ----------------------
// 原始源数据buf
uint8_t src_buf[64] = 
{
    // type=0, len=4, 后面4字节是DataA数据
    0x00,0x04, 0x11,0x22,0x33,0x44
};
// 目标接收buf
uint8_t dst_buf[64] = {0};
// ---------------------- 核心逻辑 ----------------------
int main(void)
{
    // 1.用结构体指针直接操作数组(强转)
    MsgHead_t *p_src_msg = (MsgHead_t *)src_buf;
    MsgHead_t *p_dst_msg = (MsgHead_t *)dst_buf;
    // 2.整体拷贝:头部+柔性数组全部数据
    uint32_t total_len = sizeof(MsgHead_t) + p_src_msg->len;
    memcpy(p_dst_msg, p_src_msg, total_len);
    // 3.定义两个子结构体,用来接收解析结果
    DataA_t dataA;
    DataB_t dataB;
    // 4.根据type选择性解析
    if (p_dst_msg->type == 0)
    {
        // 解析为 DataA
        memcpy(&dataA, p_dst_msg->data, sizeof(DataA_t));
        printf("解析DataA: val1=0x%04X, val2=0x%04X\n", dataA.val1, dataA.val2);
    }
    else if (p_dst_msg->type == 1)
    {
        // 解析为 DataB
        memcpy(&dataB, p_dst_msg->data, sizeof(DataB_t));
        printf("解析DataB: id=0x%08X, status=0x%02X\n", dataB.id, dataB.status);
    }
    else
    {
        printf("未知类型\n");
    }
    return 0;
}

结构体函数传地址 和 值

cpp 复制代码
// 定义一个结构体
typedef struct
{
    int x;
    int y;
}Point;
// 传值:形参是结构体本身
void testValue(Point p)
{
    p.x = 999;   // 只改副本
    p.y = 888;
    printf("传值内部:x=%d, y=%d\n", p.x, p.y);
}
// 传址:形参是结构体指针
void testAddr(Point *p)
{
    p->x = 666;  // 直接修改原变量
    p->y = 555;
    printf("传址内部:x=%d, y=%d\n", p.x, p.y);
}
int main(void)
{
    Point pt;
    pt.x = 10;
    pt.y = 20;
    // 1.传值调用
    testValue(pt);
    printf("传值后原值:x=%d, y=%d\n\n", pt.x, pt.y);
    // 2.传址调用
    testAddr(&pt);
    printf("传址后原值:x=%d, y=%d\n", pt.x, pt.y);
    return 0;
}

核心总结

  1. 传值:拷贝整个结构体,临时副本;函数内修改不影响外面;结构体大时,内存开销大、效率低
  2. 传址 (指针):只传 4/8 字节地址,效率极高;通过 -> 访问成员;函数内修改直接改动原结构体;工程 / 嵌入式全部优先用传址

三层分离、文件拆分、ops 函数指针结构体放在设备抽象层、驱动纯硬件、应用零耦合

cpp 复制代码
drv/
  drv_uart.h    底层硬件驱动声明
  drv_uart.c    底层寄存器/硬件实现
device/
  dev_uart.h    【抽象层】函数指针结构体 统一接口
  dev_uart.c    设备实例绑定、驱动注册
app/
  main.c        应用层,只调用抽象接口,不碰硬件

底层驱动层 ------ drv_uart.h

cpp 复制代码
#ifndef __DRV_UART_H
#define __DRV_UART_H
#include <stdint.h>
// 底层纯硬件函数声明
void uart1_hw_init(uint32_t baudrate);
void uart1_hw_send(uint8_t *buf, uint16_t len);
#endif

底层驱动层 ------ drv_uart.c

cpp 复制代码
#include "drv_uart.h"
// 真实项目:配置时钟、GPIO、寄存器、DMA、中断
void uart1_hw_init(uint32_t baudrate)
{
    // 硬件初始化逻辑
}
void uart1_hw_send(uint8_t *buf, uint16_t len)
{
    // 底层串口发送逻辑
    for(uint16_t i = 0; i < len; i++)
    {
        // 串口数据寄存器写入
    }
}

设备抽象层 ------ dev_uart.h 【核心】

cpp 复制代码
#ifndef __DEV_UART_H
#define __DEV_UART_H
#include <stdint.h>
// 统一操作接口:函数指针结构体
typedef struct
{
    void (*init)(uint32_t baud);
    void (*send)(uint8_t *buf, uint16_t len);
}UART_Ops_t;
// 对外暴露设备句柄
extern UART_Ops_t uart1_dev;
#endif

设备抽象层 ------ dev_uart.c

cpp 复制代码
#include "dev_uart.h"
#include "drv_uart.h"
// 设备实例:挂载底层硬件函数
UART_Ops_t uart1_dev = 
{
    .init = uart1_hw_init,
    .send = uart1_hw_send
};

应用层 ------ main.c

cpp 复制代码
#include "dev_uart.h"
int main(void)
{
    // 应用层:只调用统一抽象接口
    uart1_dev.init(115200);
    uint8_t tx_buf[] = "Hello World";
    uart1_dev.send(tx_buf, sizeof(tx_buf));
    while(1)
    {
        // 业务逻辑
    }
}

设备配置 存 Flash 掉电保存的通用写法

核心需求总结

把 PID 参数 / 配置参数 定义成一个结构体,存放在 片内 Flash 固定分区,防止掉电丢失,上电先读取 Flash 里的结构体配置。

问题:新芯片、Flash 刚擦除、首次上电、固件升级后,Flash 里全是 0xFF / 无效数据,不能直接用。

行业标准做法:在配置结构体里,多加两个关键成员:magic 魔术校验码(关键标记)、crc 校验和(防损坏、防篡改)

上电逻辑:读 Flash 结构体 → 先判断 magic 是否合法 → 再校验 CRC,不合法 / 全 FF → 自动加载默认参数,再重新写入 Flash

标准结构体设计

cpp 复制代码
// PID 配置 + 保存到Flash的配置结构体
typedef struct
{
    // 1. 【关键】魔术码:用来判断是不是有效配置
    uint32_t magic;   
    // 2. 业务参数:PID算子
    float kp;
    float ki;
    float kd;
    uint16_t dead_zone;
    int16_t  out_limit;
    // 3. 【关键】CRC校验:防止数据损坏
    uint16_t crc;
// 强制紧凑对齐,防止Flash读写字节错位
} __attribute__((packed)) Pid_Config_t;

magic:自己定一个固定数,比如 0xAA556677;Flash 空数据默认全是 0xFFFFFFFF,必然不等于合法 magic,直接就能判断:是有效配置 还是 空 / 未初始化

全局常量定义

cpp 复制代码
// 自定义魔术码(随便写一个唯一固定值)
#define PID_CONFIG_MAGIC     0xAA556677
// Flash 存储地址(STM32 片内Flash最后一页)
#define PID_CONFIG_ADDR      0x0807F000

核心逻辑伪代码

1. 上电初始化流程

cpp 复制代码
// 全局运行时PID配置
Pid_Config_t g_pid_cfg;
void PID_Config_Init(void)
{
    // 1. 直接把Flash地址强转为结构体指针
    Pid_Config_t *flash_cfg = (Pid_Config_t *)PID_CONFIG_ADDR;
    // 2. 第一步:判断 magic 是否合法
    if(flash_cfg->magic == PID_CONFIG_MAGIC)
    {
        // 3. 第二步:CRC 数据完整性校验
        if(PID_Check_Crc(flash_cfg) == OK)
        {
            // 合法:直接拷贝Flash配置到运行内存
            memcpy(&g_pid_cfg, flash_cfg, sizeof(Pid_Config_t));
            return;
        }
    }
    // 走到这里说明:
    // ① 首次上电 ② Flash被擦除 ③ 数据损坏 ④ magic非法
    // 加载默认参数
    PID_Load_Default(&g_pid_cfg);
    // 把默认参数重新写入Flash,下次上电就正常了
    PID_Save_To_Flash(&g_pid_cfg);
}

2. 加载默认参数函数

cpp 复制代码
void PID_Load_Default(Pid_Config_t *cfg)
{
    cfg->kp = 2.5f;
    cfg->ki = 0.1f;
    cfg->kd = 0.05f;
    cfg->dead_zone = 5;
    cfg->out_limit = 1000;
    // 写入合法魔术码
    cfg->magic = PID_CONFIG_MAGIC;
}

3.保存结构体到 Flash(带 CRC 计算)

cpp 复制代码
void PID_Save_To_Flash(Pid_Config_t *cfg)
{
    // 先计算CRC,填充进去
    cfg->crc = PID_Calc_Crc((uint8_t*)cfg, 
                            sizeof(Pid_Config_t) - 2);
    // STM32操作:Flash解锁 → 擦除页 → 编程写入 → 上锁
    FLASH_Unlock();
    FLASH_PageErase(PID_CONFIG_ADDR);
    FLASH_WriteData(PID_CONFIG_ADDR, (uint8_t*)cfg, sizeof(Pid_Config_t));
    FLASH_Lock();
}

4、CRC 校验、CRC 计算(简单示意)

cpp 复制代码
// 校验:对比计算出来的crc和结构体里的crc
uint8_t PID_Check_Crc(Pid_Config_t *cfg)
{
    uint16_t calc_crc = PID_Calc_Crc((uint8_t*)cfg, 
                                     sizeof(Pid_Config_t) - 2);
    return (calc_crc == cfg->crc) ? 1 : 0;
}
核心问题解答
  1. 怎么判断结构体是空 / 无效?靠 magic 魔术码,新 Flash / 擦除后:magic = 0xFFFFFFFF,正常配置:magic = 固定约定值 0xAA556677
  2. 为什么还要加 CRC?防止电压波动、Flash 位翻转、意外断电导致参数半截损坏,光判断 magic 不够,必须做完整性校验
  3. 是不是工程标准写法?100% 就是 STM32 工业设备、控制器、变频器、温控仪通用写法,适用于掉电保存参数、首次上电默认值、固件升级不兼容、Flash 清空容错场景
STM32 内核硬件特性

结构体成员偏移<128B = 短指令、快访问;高频数据前置,是裸机实时控制、调速、PID 优化的常规标准写法。

结构体嵌套赋值

cpp 复制代码
// 内层结构体
struct Info {
    int age;
    char sex;
};
// 外层结构体 嵌套 Info
struct User {
    int id;
    // 嵌套成员
    struct Info info;
};
struct User u2 = {
    20,
    {18, 'F'}   // 内层结构体整体初始化
};

结构体重定义

先写通用 Sensor 基础结构体,所有传感器公共属性:通道、延时、状态

cpp 复制代码
// 通用传感器基础属性
typedef struct
{
    uint16_t ch_val;    // 通道采样值
    uint16_t delay_ms;  // 延时时间
    uint8_t  state;     // 运行状态
} SensorBase_t;

// 给不同传感器单独起类型名,语义清晰
typedef SensorBase_t TempSensor_t;    // 温度传感器
typedef SensorBase_t PressSensor_t;   // 压力传感器
typedef SensorBase_t LightSensor_t;    // 光照传感器

TempSensor_t    temp_sen;
PressSensor_t   press_sen;
LightSensor_t   light_sen;

优势:公共参数抽通用结构体,所有传感器复用;用 typedef 起不同别名,区分设备语义;核心价值:解耦、易维护、易扩展、可读性强、少 BUG;

Linux 通过成员求结构首地址解析

cpp 复制代码
#define offsetof(type, member)  ((size_t)&((type *)0)->member)
#define container_of(ptr, type, member)                \
({                                                     \
    const typeof(((type *)0)->member) *__mptr = ptr;  \
    (type *)((char *)__mptr - offsetof(type, member)); \
})

参数:ptr:已知的【成员指针】,你现在手里拿到的、结构体里某个成员的地址。

type:完整结构体类型,你最终要反向拿到的「整个大结构体」类型。member:ptr 对应的、该成员在结构体内的成员名。

typeof (((type ) 0)->member):GCC 扩展语法,自动识别这个成员的原生类型 char * 严格按 1 字节运算 注:结构体首地址 = 成员地址 - 成员偏移量,char 保证字节减法、typeof 保证类型安全。

typeof 和 sizeof 都是在编译处理

cpp 复制代码
int fun(int a);
typeof(fun) * fptr;    // int (*fptr)(int);
typeof(int *)a, b;     // int *a, *b;
typeof(int) * a, b;    // int *a, b;

结构体位于操作

1、真实位域 + 真实寄存器地址

cpp 复制代码
// 引脚4位位域(真实寄存器划分)
typedef struct
{
    uint32_t CNF:2;
    uint32_t MODE:2;
}GPIO_PinBit;
// 真实 CRH 结构体映射
typedef struct
{
    GPIO_PinBit PIN8;
    GPIO_PinBit PIN9;
    GPIO_PinBit PIN10;
    GPIO_PinBit PIN11;
    GPIO_PinBit PIN12;
    GPIO_PinBit PIN13;  // 常用PC13
    GPIO_PinBit PIN14;
    GPIO_PinBit PIN15;
}GPIO_CRH_REG;
// 关键:F103 真实寄存器绝对地址
#define GPIOC_CRH  ((GPIO_CRH_REG *)0x40011004)
#define GPIOC_ODR  ((uint32_t *)0x4001100C)

2、真实操作(PC13 点灯,工业裸机写法)

cpp 复制代码
// 1. 配置 PC13 推挽输出(直接操作位域,无移位)
GPIOC_CRH->PIN13.CNF  = 0;
GPIOC_CRH->PIN13.MODE = 1;
// 2. 直接操作输出位
*GPIOC_ODR |= (1 << 13);  // 亮
*GPIOC_ODR &=~(1 << 13);  // 灭

对比 传统寄存器 vs 位域

cpp 复制代码
// 传统写法(要算移位、极易写错)
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |=  (0x3 << 20);
// 位域写法(真实可用)
GPIOC_CRH->PIN13.CNF  = 0;
GPIOC_CRH->PIN13.MODE = 1;
精简好处(背这 3 条)
  1. 寄存器按硬件真实位分段,不用手动算移位掩码;
  2. 只改当前引脚位,不影响其他引脚;
  3. 直接地址操作,比 HAL / 库函数更快、延时最小。

结构体位域控内存

cpp 复制代码
// 对应寄存器 1字节 8位:bit0 ~ bit7
struct Reg8
{
    uint8_t b0 : 1;   // 先定义 → 绑定 bit0(最低位)
    uint8_t b1 : 1;   // 绑定 bit1
    uint8_t b2 : 1;   // 绑定 bit2
    uint8_t b3 : 1;
    uint8_t b4 : 1;
    uint8_t b5 : 1;
    uint8_t b6 : 1;
    uint8_t b7 : 1;   // 后定义 → 绑定 bit7(最高位)
};

STM32 / ARM 小端:位域先定义的成员 → 对应寄存器 低位 bit0;位域 :n :严格占用 n 个二进制位,压缩内存、贴合硬件。

7、展开联合体

多视角解读 32 位数据的联合体

cpp 复制代码
typedef union {
    uint32_t a;              // 视角1:把这 4 字节看成一个 32 位整数
    uint8_t  b[4];           // 视角2:看成 4 个独立字节
    uint16_t c[2];           // 视角3:看成 2 个 16 位半字
    struct {
        uint16_t x;
        uint16_t y;
    } d;                     // 视角4:看成两个 16 位成员的结构体
    struct {
        uint32_t arg1 : 5;   // 视角5:按位域分解成 6 个独立字段
        uint32_t arg2 : 6;
        uint32_t arg3 : 5;
        uint32_t arg4 : 3;
        uint32_t arg5 : 11;
        uint32_t arg6 : 2;
    } e;
} x;
  • 这个联合体的总大小永远是 4 字节(32 bit),所有成员共享同一块内存。
  • 给 aaa.a = 0x12345678 赋值后,后面所有成员看到的,都是同一 4 字节数据,只是 "解读方式不同"。

逐段分析代码作用

  1. 用 uint32_t a 视角:aaa.a = 0x12345678; 直接把这 4 字节当成一个完整的 32 位无符号整数,数值就是 0x12345678。
  2. 用 uint8_t b [4] 视角(按字节拆分):printf ("0X%02x 0X%02x 0X%02x 0X%02x\r\n", aaa.b [3], aaa.b [2], aaa.b [1], aaa.b [0]);小端序(ARM/STM32 默认)下内存:地址低→高: 0x78 0x56 0x34 0x12,对应数组下标: b [0] b [1] b [2] b [3],打印结果:0X12 0X34 0X56 0X78,作用:查看内存字节序、网络字节序转换、按字节操作数据。
  3. 用 uint16_t c [2] 和 struct d 视角(按 16 位拆分):printf ("0X%04x 0X%04x\r\n", aaa.c [1], aaa.c [0]);printf ("0X%04x 0X%04x\r\n", aaa.d.x, aaa.d.y);小端序内存: 0x78 0x56 0x34 0x12,c [0] = 0x5678,c [1] = 0x1234,打印结果:0X1234 0X5678,d.x 和 d.y 和 c [0]、c [1] 完全等价,作用:把 32 位寄存器 / 数据拆成高低 16 位,单独操作。
  4. 用 struct e 视角(位域分解):struct 把 32 bit 分成 6 个不同长度字段,赋值后可直接读取每个字段,不用移位和掩码,作用:直接按位操作寄存器 / 协议字段。
代码的核心作用

一句话概括:一块 4 字节的内存,既可以看成一个整数,也可以看成字节数组、半字数组、位域结构,不用拷贝数据,就能从不同角度解读它,即联合体的多面性。

应用场景(嵌入式 / 单片机)
  1. 寄存器映射:一次性写整个寄存器,也可直接修改某一字段
  2. 网络协议 / 通信帧解析:直接按字段访问,无需手动解析
  3. 数据类型转换(无拷贝):类型双关,比强制转换更安全清晰
  4. 大小端转换、字节序处理:按字节重排数据

联合体通信协议经典用法

cpp 复制代码
typedef union {
    struct {
        uint8_t header;      // 帧头
        uint8_t datatype;    // 数据类型
        uint16_t len;        // 数据长度
        uint16_t data_buf[0];// 柔性数组,存实际数据
    } frame;
    uint8_t buf[100];         // 字节数组,用来直接接收/发送原始数据
} data_frame;
  • 联合体里的 frame 和 buf 共享同一块内存
  • 接收数据:通过 x.buf 读原始字节流,解析用 x.frame 访问字段
  • 核心作用:数据收发零拷贝、通信协议一键解析
  • 应用场景:单片机串口 / RS485/CAN 通信、Modbus / 自定义物联网协议、低资源嵌入式系统

柔性数组联合体示例

cpp 复制代码
typedef struct
{
    uint8_t header;
    uint8_t datatype;
    uint16_t len;
    uint16_t data_buf[0];
} data_frame;
uint8_t buf[100];
((data_frame *)(buf))->data_buf

data_buf [0] 是柔性数组,不占结构体额外空间,强转后自动指向结构体头后的数据部分

大小端判断

cpp 复制代码
// 方法1:用联合体判断大小端
union {
    uint16_t a;
    uint8_t byte[2];
} val = {.a = 0x1234};
val.byte[0]
// 方法2:用指针强制转换判断大小端
uint16_t a = 0x1234;
((uint8_t *)&a)[0];

应用:串口 / 网络通信字节序转换

八、展开枚举

  1. 结构体 + 数组:用数字下标可读性差,调整顺序易出错
  2. #define 宏定义:文本替换,无类型检查,易重复定义、作用域污染
  3. enum 枚举(推荐):自动递增整数,有类型、作用域,安全规范
cpp 复制代码
enum 
{
    LILEI,      // 0
    HANMEIMEI,  // 1
    MADONGMEI,  // 2
    LINING,     // 3
    LILU,       // 4
    TEILANGPU,  // 5
    BAIDENG     // 6
};

核心结论:给数组下标赋予语义化名字,提升可读性和可维护性;#define 无类型检查,enum 是标准规范写法

命令码定义

cpp 复制代码
enum
{
    OPENDOOR = 1000,
    CLOSEDOOR,
    CLEAN,
    RESET,
    ......
};

九、表达式和运算符进阶挑战

  • A = 10;B = 10;B += B -= B * B + A;结果:-200
  • float a = 1.0f;int b = 0; // 关键:b 默认考题里都是给 0
  • printf ("% d", a + b);
    • float 会被提升为 double
    • C 语言可变参数(printf 里):只要是 float,一律默认隐式提升成 double
    • a + b:int b 转 float → 相加 → 结果 float → 传入 printf → 自动升级为 double
    • 关键②:double 内存布局 + % d 冲突% d 要求:拿低 32 位二进制当成 int 解析double 是 8 字节 (64 位) 标准布局:符号位 (1) + 指数 (11) + 尾数 (52)double 1.0 的二进制内存:高 32 位有指数、符号位,低 32 位全部都是 0
  • uint8_t a = 100;uint8_t b = 100;打印 a*b 结果不是 10000,而是会发生截断
    • uint8_t 取值范围 0~255 (2 的八次方减一)
    • uint16_t 0~65535
    • 最高位表示正负,所以 7 次方
    • int8_t -128~127 (2 的七次方 - 1)
    • int16_t -32768~32767
  • 死循环
  • int8_t a = 0;
  • while (a <= 255){a++;}
  • x << n 等价于 x * 2ⁿ
  • 所以 1 * 8 等价于 1 << 3
  • int8_t a = -3 的二进制(补码)
    • a >> 1 结果
    • 反码 +1(求补码):1111 1101
    • 按位取反(求反码):1111 1100
    • 正数 3 的 8 位原码:0000 0011
  • 有符号右移 >> 1 的计算步骤核心规则:有符号数右移,高位补符号位(负数补 1)
    • 原补码:1111 1101
    • 右移 1 位(挤掉最右边的 1,左边补 1):1111 1110
    • 把 1111 1110 转回十进制(补码逆运算):补码减 1:1111 1101按位取反:0000 0010(数值为 2)加负号:-2
  • 负数左移就是严格乘 2,和正数规则一致
  • 负数右移算术右移,向下取整,和 C 语言 / 除法不一样
  • STM32 加减乘都是几十 ps 的量级,但除法和取模直接冲到了 1200ps 左右,比加法慢了近 30 倍,比赋值慢了 1500 倍
  • x / 位 %10 = x 对应位值每一位对应在数码管显示

负数取模

cpp 复制代码
int array [] = {-3, -2, -1, 0, 1, 2, 3};
int i;
for (i = 0; i < 7; i++) {
// 判断是否为偶数
if (array [i] % 2 == 0) {
printf ("% d", array [i]);
}
}

不管正负,偶数对 2 取模的结果永远是 0,奇数才会得到 ±1。

  • // 关键:每 8 个数字,就输出一次换行,否则输出空格
  • printf ((((i+1)%8)?"":"\r\n"));

用三目运算符处理 "尾巴数据"

cpp 复制代码
(12768%512) ? FLASH_Write_Sector (i, buf+i512, (12768%512)) : 0;
//这行的作用是:判断是否有剩余数据,如果有,就单独写一次;如果没有,就什么都不做。
  • 把一个 0~255 的无符号字符型数据,转换成 0xXX 格式的十六进制字符串,并存入 str 中。
cpp 复制代码
void Value2String (unsigned char value,char *str)
{
    char *Hex_Char_Table="0123456789ABCDEF";
    str [0]='0';str [1]='X';str [4]=0;
    str [2]=Hex_Char_Table [value/16];
    str [3]=Hex_Char_Table [value%16];
}

uint8_t 转二进制

cpp 复制代码
uint8_t reg = 0X5A;
uint8_t i = 0;
char bstr[9];
printf("0b");
for(i=0; i<8; i++)
{
    bstr[7-i] = "01"[reg%2];
    reg /= 2;
}
printf(bstr);

比较浮点类型

cpp 复制代码
typedef union
{
    float f;
    uint8_t bytes[4];
} FLOAT;
FLOAT uf;
FLOAT xf;
int i=0;
for(i=0;i<9;i++) uf.f += +0.0;
xf.f = -0.0;
if(xf.f == uf.f) printf("相等\r\n");
else printf("不相等\r\n");
printf("uf:%02X %02X %02X %02X\r\n",uf.bytes[3],uf.bytes[2],uf.bytes[1],uf.bytes[0]);
printf("xf:%02X %02X %02X %02X\r\n",xf.bytes[3],xf.bytes[2],xf.bytes[1],xf.bytes[0]);
  • 在 IEEE 754 浮点数标准中,+0.0 和 -0.0 的二进制表示是不同的:+0.0:符号位为 0,十六进制为 00 00 00 00-0.0:符号位为 1,十六进制为 80 00 00 00
  • 但在数值上,+0.0 == -0.0 的结果为真,因为它们代表的数学值是相等的。
  • 作用:利用联合体的内存共享特性,既可以按 float 类型访问数值,也可以按 uint8_t bytes [4] 数组访问浮点数的原始二进制字节。

反向判断

cpp 复制代码
int a=0;
if(1==a)
{
    //代码
}

短路求值

cpp 复制代码
int i = 10;
int j = 20;
int k, m, c;
// && 的短路(左边为假,右边不执行)
m = (i > j) && (j = 0);
printf("m = %d, j = %d\n", m, j);
// || 的短路(左边为真,右边不执行)
c = (i < j) || (k = 8);
printf("c = %d, k = %d\n", c, k);

结果:m = 0, j = 20 c = 1, k = 随机值

位运算规则

  • & 全 1 为 1
  • ^ 相同为 0 不同为 1
  • | 有 1 为 1 全 0 为 0
  • 1^2^2^3^4......n = 2
  1. 一个数组存放了若干个整数,一个数出现了奇数次,其余数都出现了偶数次,找出这个奇数次的数
cpp 复制代码
int findOddNum(int *arr, int len)
{
    int res = 0;
    for(int i = 0; i < len; i++)
    {
        res ^= arr[i];
    }
    return res;
}

51 模拟 spi 驱动速度大约在 300~500kbps

cpp 复制代码
#include <REG51.H>
// 定义 SPI 引脚(根据你的硬件修改)
sbit SPI_SCK = P1^0;  // 时钟
sbit SPI_MOSI = P1^1; // 主机输出,从机输入(SI)
sbit SPI_MISO = P1^2; // 主机输入,从机输出(SO)
sbit SPI_CS = P1^3;   // 片选
// 可位寻址变量,方便取位
unsigned char bdata spi_dat;
sbit spi_dat7 = spi_dat^7;
sbit spi_dat6 = spi_dat^6;
sbit spi_dat5 = spi_dat^5;
sbit spi_dat4 = spi_dat^4;
sbit spi_dat3 = spi_dat^3;
sbit spi_dat2 = spi_dat^2;
sbit spi_dat1 = spi_dat^1;
sbit spi_dat0 = spi_dat^0;

/**
 * @brief  SPI 全双工收发一个字节
 * @param  txdata: 要发送的字节
 * @retval 接收到的字节
 */
unsigned char SPI_RW_Byte(unsigned char txdata)
{
    unsigned char rxdata = 0;
    spi_dat = txdata; // 把要发送的数据放到可位寻址变量
    // 发送第7位,同时接收
    SPI_MOSI = spi_dat7;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x80;
    SPI_SCK = 1;
    // 发送第6位,同时接收
    SPI_MOSI = spi_dat6;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x40;
    SPI_SCK = 1;
    // 发送第5位,同时接收
    SPI_MOSI = spi_dat5;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x20;
    SPI_SCK = 1;
    // 发送第4位,同时接收
    SPI_MOSI = spi_dat4;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x10;
    SPI_SCK = 1;
    // 发送第3位,同时接收
    SPI_MOSI = spi_dat3;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x08;
    SPI_SCK = 1;
    // 发送第2位,同时接收
    SPI_MOSI = spi_dat2;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x04;
    SPI_SCK = 1;
    // 发送第1位,同时接收
    SPI_MOSI = spi_dat1;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x02;
    SPI_SCK = 1;
    // 发送第0位,同时接收
    SPI_MOSI = spi_dat0;
    SPI_SCK = 0;
    if(SPI_MISO) rxdata |= 0x01;
    SPI_SCK = 1;
    return rxdata;
}

/**
 * @brief  SPI 发送一个字节(只发不收)
 */
void SPI_Send_Byte(unsigned char dat)
{
    SPI_CS = 0; // 拉低片选
    SPI_RW_Byte(dat);
    SPI_CS = 1; // 拉高片选
}

/**
 * @brief  SPI 接收一个字节(只收不发,发0xFF)
 */
unsigned char SPI_Recv_Byte(void)
{
    unsigned char dat;
    SPI_CS = 0;
    dat = SPI_RW_Byte(0xFF);
    SPI_CS = 1;
    return dat;
}

实战:GPIO 位带

cpp 复制代码
// 定义 PAout(n):直接操作 GPIOA->ODR 的第 n 位
#define PAout(n)   BIT_ADDR((uint32_t)&GPIOA->ODR, n)
#define PAin(n)    BIT_ADDR((uint32_t)&GPIOA->IDR, n)
// 用法
PAout(5) = 1;   // PA5 置 1(硬件原子)
PAout(5) = 0;   // PA5 清 0
if (PAin(5)) {} // 读 PA5 输入
  • 原子操作:单指令完成,不会被中断打断,多任务 / 中断安全
  • 代码极简:像 51 一样 X=1/X=0,可读性强
  • 效率高:省去读‑改‑写,减少总线访问、CPU 周期
  • 无竞争:适合频繁翻转 IO、标志位、中断标志

注意

  • 只支持 Cortex‑M3/M4(M0/M0+ 没有位带)
  • 仅限上述 2 个 1MB 区域,不是整个内存
  • SRAM 位带区范围:0x20000000 ~ 0x200FFFFF(1MB)别名区:0x22000000 ~ 0x23FFFFFF(32MB)
  • 外设位带区(GPIO、串口、定时器等寄存器)范围:0x40000000 ~ 0x400FFFFF(1MB)别名区:0x42000000 ~ 0x43FFFFFF(32MB)
  • 写别名区任意值:最低位决定 0/1,高位忽略
  • 现代 HAL 也有 HAL_GPIO_WritePin,但位带 更高效、更原子

闭包表达式

cpp 复制代码
#define CAL_CHKSUM(buf, len) ({int i=0, sum=0; \
for(i=0;i<len;i++) sum+=buf[i];sum;})

说明:这是 GCC 扩展的 ** 语句表达式(statement expression)** 宏,整体放在一对圆括号 ({...}) 里,内部可以有多条语句、循环等,最后一个表达式 sum; 作为整个宏的 "返回值"。

C 预处理器的字符串化 # 和拼接 ##,属于高级宏技巧

cpp 复制代码
#define RTM_EXPORT(symbol)                                           \
const char __rtmsym_##symbol##_name[] = #symbol;                      \
const struct rt_module_symtab __rtmsym_##symbol SECTION("RTMSymTab") = { \
    (void *)&symbol,                                                  \
    __rtmsym_##symbol##_name                                         \
};

SECTION 就是编译器会:专门开辟一块内存段,名字叫 RTMSymTab然后把这个结构体单独放进去。

位图

场景 1:一千万人的性别标记需求:标记 1000 万人的性别(0 = 男,1 = 女)。计算:1000 万 ÷ 32 ≈ 312500 个 int空间:约 1.25MB,相比直接用数组存节省了上千倍空间。操作:通过位运算(& | ^)快速读写单个状态。

场景 2:40 亿整数的存在性判断需求:给 40 亿个不重复的 unsigned int,快速判断一个数是否在其中。计算:unsigned int 范围是 0~2³²-1(约 42 亿),刚好可以用一个大小为 2³² bits 的位图覆盖。空间:2³² bits = 512MB,内存可承受。实现步骤:遍历 40 亿个数,把对应 bit 置 1。查询时,直接访问该数对应的 bit,0 表示不存在,1 表示存在

关键优势

  • 极致省空间:1bit / 对象,处理海量数据时比数组 / 哈希表节省几个数量级的内存。
  • 查询极快:时间复杂度 O (1),直接定位 bit 位判断状态。
  • 操作高效:所有读写都可通过位运算完成,CPU 执行效率高。

适用场景

  • 海量数据去重
  • 快速存在性判断(黑名单、白名单)
  • 状态标记(用户在线状态、任务完成状态)
  • 排序(位图排序)

位图实现代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
// 位图结构体
typedef struct {
    unsigned int *data;  // 数据数组,每个元素存32个bit
    int size;            // 能表示的最大元素个数
} BitMap;

// 1. 初始化位图,size为需要表示的最大数字
BitMap* bitmap_create(int size) {
    BitMap *bm = (BitMap*)malloc(sizeof(BitMap));
    if (!bm) return NULL;
    // 计算需要多少个unsigned int(向上取整)
    int array_size = (size + 31) / 32;
    bm->data = (unsigned int*)calloc(array_size, sizeof(unsigned int));
    bm->size = size;
    return bm;
}

// 2. 把数字x标记为存在(置1)
void bitmap_set(BitMap *bm, int x) {
    if (x < 0 || x >= bm->size) return;
    int idx = x / 32;        // 数组下标
    int bit = x % 32;        // 位偏移
    bm->data[idx] |= (1U << bit);
}

// 3. 判断数字x是否存在(读bit)
int bitmap_test(BitMap *bm, int x) {
    if (x < 0 || x >= bm->size) return 0;
    int idx = x / 32;
    int bit = x % 32;
    return (bm->data[idx] & (1U << bit)) != 0;
}

// 4. 把数字x标记为不存在(置0)
void bitmap_clear(BitMap *bm, int x) {
    if (x < 0 || x >= bm->size) return;
    int idx = x / 32;
    int bit = x % 32;
    bm->data[idx] &= ~(1U << bit);
}

// 5. 销毁位图
void bitmap_destroy(BitMap *bm) {
    if (bm) {
        free(bm->data);
        free(bm);
    }
}

// 测试主函数
int main() {
    // 比如:处理范围0~99的数字
    BitMap *bm = bitmap_create(100);
    // 标记几个数字
    bitmap_set(bm, 10);
    bitmap_set(bm, 25);
    bitmap_set(bm, 99);
    // 查询
    printf("10 是否存在:%s\n", bitmap_test(bm, 10) ? "是" : "否");
    printf("20 是否存在:%s\n", bitmap_test(bm, 20) ? "是" : "否");
    printf("99 是否存在:%s\n", bitmap_test(bm, 99) ? "是" : "否");
    // 清除10
    bitmap_clear(bm, 10);
    printf("清除后,10 是否存在:%s\n", bitmap_test(bm, 10) ? "是" : "否");
    bitmap_destroy(bm);
    return 0;
}

位图排序

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
// 位图结构体
typedef struct {
    unsigned int *data;
    int size;  // 位图可表示的最大数+1
} BitMap;

// 创建位图
BitMap* bitmap_create(int max_num) {
    BitMap *bm = (BitMap*)malloc(sizeof(BitMap));
    if (!bm) return NULL;
    int array_size = (max_num + 31) / 32; // 向上取整
    bm->data = (unsigned int*)calloc(array_size, sizeof(unsigned int));
    bm->size = max_num + 1;
    return bm;
}

// 设置bit
void bitmap_set(BitMap *bm, int x) {
    if (x < 0 || x >= bm->size) return;
    int idx = x / 32;
    int bit = x % 32;
    bm->data[idx] |= (1U << bit);
}

// 销毁位图
void bitmap_destroy(BitMap *bm) {
    if (bm) {
        free(bm->data);
        free(bm);
    }
}

// 位图排序(去重版)
void bitmap_sort(int arr[], int n, int max_num) {
    BitMap *bm = bitmap_create(max_num);
    if (!bm) return;
    // 1. 标记所有数字
    for (int i = 0; i < n; i++) {
        bitmap_set(bm, arr[i]);
    }
    // 2. 按顺序输出
   //idx:找到数字 i 存在哪个 int 里
   //bit:找到在这个 int 的第几位
    printf("排序结果:");
    for (int i = 0; i <= max_num; i++) {
        int idx = i / 32;
        int bit = i % 32;
        if ((bm->data[idx] & (1U << bit)) != 0)/*判断这个是否存在*/ {
            printf("%d ", i);
        }
    }
    printf("\n");
    bitmap_destroy(bm);
}

int main() {
    int arr[] = {5, 3, 7, 1, 3, 9, 0, 10};
    int n = sizeof(arr) / sizeof(arr[0]);
    int max_num = 10; // 已知最大值
    bitmap_sort(arr, n, max_num);
    return 0;
}

十、深度刨析 C 语言指针

  • C 语言指针就是有类型的地址!!
  • (uint8_t *)(0X12345678)uint8_t * 指向 1 字节0X12345678 32 位地址*(uint8_t *)(0X12345678) = 0X55
  • 字符串、数组、结构体首地址是常量不能 ++、--
  • 指针是变量可以 ++、--

共用体指针

cpp 复制代码
typedef union
{
    float a;
    uint8_t bytes[4];
} data_uni;
data_uni x = {.a = 1.0};
data_uni *p = &x;
p->bytes[3] = 0X80;
printf("%f", p->a);

求 float 相反数结果 - 1

C 语言中动态内存分配(malloc)的使用原则与注意事项

  • 优先静态分配:能用编译期就确定大小的静态数组(如 int array [100])就别用动态分配。
  • 必须配对释放:用了 malloc 申请的内存,一定要用 free 释放,避免内存泄漏。
  • 检查返回值:调用 malloc 后要判断是否返回 NULL,防止空指针访问崩溃。
  • 避免频繁申请释放:频繁 malloc/free 会产生内存碎片,降低性能。
  • 合理配置堆大小:根据实际需求设置堆(heap)的大小,避免资源不足或浪费。

STM32 单片机 IAP(在应用编程)中,bootloader 跳转到用户应用程序的标准流程代码,实现从 Bootloader 跳转到 APP 执行

cpp 复制代码
// 定义一个无返回值、无参数的函数指针类型iapfun
typedef void (*iapfun)(void);
iapfun jump2app;  // 声明一个该类型的函数指针变量
// 定义volatile修饰的uint32_t类型,用于防止编译器优化,确保每次都从内存读取
typedef volatile uint32_t vu32;

void MSR_MSP(uint32_t addr)
{
    // set Main Stack value
    __ASM volatile("MSR MSP, r0");
    __ASM volatile("BX r14");
}

void iap_load_app(uint32_t appxaddr)
{
    disable_irq();  // 关闭所有中断,防止跳转时中断干扰
    // 1. 获取APP的复位入口地址(程序开始地址)
    // 用户代码区第二个字(appxaddr+4)存放的是复位向量地址
    jump2app = (iapfun)*(vu32*)(appxaddr + 4);
    // 2. 初始化APP的栈顶指针
    // 用户代码区第一个字(appxaddr)存放的是栈顶地址
    MSR_MSP(*(vu32*)appxaddr);
    // 3. 跳转到APP的复位入口,执行用户程序
    jump2app();
}

MSR MSP, r0:把传入的 addr(r0 寄存器)的值写入主栈指针寄存器 MSP。

BX r14:函数返回,跳回调用处。

  • disable_irq ():关闭全局中断,避免跳转过程中被中断打断,导致程序跑飞。
  • 读取 appxaddr+4:APP 程序的向量表中,偏移 4 字节的位置是复位中断的入口地址。
  • 读取 appxaddr:APP 向量表起始位置存放的是 APP 的栈顶地址,必须先设置 MSP。
  • jump2app ():通过函数指针,跳转到 APP 的复位入口,开始执行用户程序。

柔性数组指针

cpp 复制代码
typedef struct
{
    uint8_t header;
    uint16_t data_type;
    uint8_t pbuf[];  // 柔性数组(长度可变)
} data_frame;
// 一次malloc,同时分配结构体和后面的数据缓冲区
data_frame *p = (data_frame *)malloc(sizeof(data_frame) + datalen);
// 从串口1接收数据,直接存入柔性数组对应的内存区域
uart1_receive_data(p->pbuf, datalen);

数组与字符串

cpp 复制代码
int realarray[10];
int *array = &realarray[-1];
//取 realarray 地址往前偏移 1 个 int 后的地址

分割长字符串

cpp 复制代码
unsigned char substr(unsigned char *pos, char *str)
{
    unsigned char len = strlen(str);
    unsigned char n = 0, i = 0;
    // 遍历字符串,遇到空格时做标记
    for (; i < len; i++)
    {
        if (str[i] == ' ')
        {
            str[i] = 0;          // 把空格替换成'\0',让前面的子串成为独立字符串
            pos[n++] = (i + 1);  // 记录下一个子串的起始偏移量
        }
    }
    return n; // 返回子串的总数
}
// 使用示例
unsigned char pos[10];  // 最多存10个子串的偏移量
char str[30];
strcpy(str, "abc 1000 50 off 2500");
unsigned char count = substr(pos, str);
// 访问拆分后的子串
str + pos[0];  // "abc"
str + pos[1];  // "1000"
str + pos[2];  // "50"
str + pos[3];  // "off"
str + pos[4];  // "2500"

同类写法

cpp 复制代码
// 字符数组定义(注意:这里数组长度10明显不足以放下所有元素,存在笔误)
char str1[10] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
// 字符串定义(自动在末尾加上'\0'结束符)
char *str2 = "0123456789ABCDEF";

C 语言里,字符串 + 十六进制转义符 \x,可以表示任意 8 位数据序列,不局限于 ASCII 字符

cpp 复制代码
// 1. 定义一个uint8_t数组,存任意字节数据
uint8_t buf[10] = {0x34, 0x78, 0x90, 0x12, 0x37, 0x27, 0x29, 0x10, 0x45, 0x38};
// 2. 用字符串+转义符表示完全相同的字节序列
"\x34\x78\x90\x12\x37\x27\x29\x10\x45\x38"
// 3. 直接把字符串当数组用
return "\x34\x89"[value];  // 当value=1时,返回0x89
printf("%f\n", ((float *)"\x1B\x0D\xA9\x41")[0]);

\x1B\x0D\xA9\x41 4 字节float * 指向 4 字节,

\x1B\x0D\xA9\x41 的浮点结果就是答案

数组区间初始化

cpp 复制代码
#define MAX (10)
int buf[MAX] = {
    0XAA, 0X55,          // 前两个元素
    [MAX-3] = 0X12, 0X34, 0X56  // 从索引 MAX-3 开始初始化
};

十一、函数

断言

cpp 复制代码
#ifdef USE_FULL_ASSERT
  #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0)
#endif
  • 执行 (void) 0,什么都不做
  • 调用 assert_failed 函数,传入当前文件名 FILE 和行号 LINE,方便定位错误
cpp 复制代码
void assert_failed(uint8_t* file, uint32_t line)
{
    // 死循环,触发断点
    while (1)
    {
        // 可在此处添加错误日志、串口打印等调试信息
    }
}

STM32 外设参数合法校验

cpp 复制代码
#define IS_GPIO_ALL_PERIPH(PERIPH) ( \
    (((*(uint32_t*)&(PERIPH)) == GPIOA_BASE) || \
     ((*(uint32_t*)&(PERIPH)) == GPIOB_BASE) || \
     ((*(uint32_t*)&(PERIPH)) == GPIOC_BASE) || \
     ((*(uint32_t*)&(PERIPH)) == GPIOD_BASE) || \
     ((*(uint32_t*)&(PERIPH)) == GPIOE_BASE) || \
     ((*(uint32_t*)&(PERIPH)) == GPIOF_BASE) || \
     ((*(uint32_t*)&(PERIPH)) == GPIOG_BASE)) \
)
// 典型用法
void GPIO_SetMode(GPIO_TypeDef *GPIOx, uint16_t Pin, uint8_t Mode)
{
    // 用宏检查GPIOx是否合法
    assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    // ... 后续操作
}

printf 可以同时向多个串口(UART1/2/3)输出

cpp 复制代码
// 全局变量:记录当前要输出的串口
unsigned char us=0;
// 重定向printf的底层字符输出函数
int fputc(int ch, FILE *f)
{
    switch(us)
    {
        case 0: // UART1:调试串口
            while((USART1->SR & 0x40) == 0); // 等待发送寄存器为空
            USART1->DR = ch;
            break;
        case 1: // UART2:ESP8266通信
            while((USART2->SR & 0x40) == 0);
            USART2->DR = ch;
            break;
        case 2: // UART3:SIM800通信
            while((USART3->SR & 0x40) == 0);
            USART3->DR = ch;
            break;
    }
    return ch;
}
// 宏定义:切换当前printf输出的目标串口
#define U_TO_DEBUG     us=0;
#define U_TO_ESP8266   us=1;
#define U_TO_SIM800    us=2;
// 使用示例
int main(void)
{
    // 向调试串口输出
    U_TO_DEBUG
    printf("hello world!");
    // 向ESP8266发送AT指令
    U_TO_ESP8266
    printf("AT\r\n");
    // 向SIM800发送AT指令
    U_TO_SIM800
    printf("AT\r\n");
}

可变参数宏实现

cpp 复制代码
typedef char * va_list;
// 数据类型的长度计算,四字节对齐
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
// 把ap指针移动到v的最后面
#define va_start(ap, v) ( ap = (va_list)&v + _INTSIZEOF(v) )
// 返回ap内的数值,然后将ap指针后移(图中未写全)
#define va_arg(ap, t) ( *(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
// 防止野指针的存在
#define va_end(ap) ( ap = (va_list)0 )

宏定义封装日志打印

cpp 复制代码
#define LOG_ENABLE (1)
#define LOG(ARGS) { if(LOG_ENABLE) printf(ARGS); else 0; }
#define LOG_ENABLE (1)
#define LOG(fmt, ...) do { \
    if(LOG_ENABLE) { \
        printf("[LOG] %s:%d: " fmt "\r\n", __FILE__, __LINE__, ##__VA_ARGS__); \
    } \
} while(0)
// 使用方式:和printf完全一样
LOG("初始化完成,值=%d", 100);

多级日志

cpp 复制代码
#define LOG_LEVEL_NONE  0
#define LOG_LEVEL_ERROR 1
#define LOG_LEVEL_INFO  2
#define LOG_LEVEL_DEBUG 3
#define LOG_LEVEL LOG_LEVEL_DEBUG
#define LOG_ERROR(fmt, ...) do { if(LOG_LEVEL >= LOG_LEVEL_ERROR) printf("[ERROR] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_INFO(fmt, ...)  do { if(LOG_LEVEL >= LOG_LEVEL_INFO)  printf("[INFO] " fmt "\r\n", ##__VA_ARGS__); } while(0)
#define LOG_DEBUG(fmt, ...) do { if(LOG_LEVEL >= LOG_LEVEL_DEBUG) printf("[DEBUG] " fmt "\r\n", ##__VA_ARGS__); } while(0)

内联函数

inline 是 C 语言的关键字,它的作用是建议编译器将函数的代码直接插入到调用位置用 inline 内联函数优化选择排序

cpp 复制代码
// 内联函数:交换两个float变量的值
inline void swapf(float *p1, float *p2)
{
    float tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
// 选择排序函数:对长度为n的float数组进行升序排序
void selection_sortf(float a[], int n)
{
    int i, j, mini;
    for (i = 0; i < n - 1; ++i)
    {
        mini = i; // 记录当前最小值的索引
        // 从i后面的元素中找最小值
        for (j = i + 1; j < n; j++)
        {
            if (a[j] < a[mini])
                mini = j;
        }
        // 交换最小值元素和当前i位置的元素
        swapf(a + i, a + mini);
    }
}
// 错误:声明为 extern inline
extern inline void swapf(float *p1, float *p2);

程序编译过程

第一阶段:预处理(Preprocess)

文件后缀.c.i做的事情:

  1. 删掉注释
  2. 展开#include头文件
  3. 替换#define宏定义
  4. 处理#if/#else条件编译特点 :还是纯文本代码,没有翻译成机器码

第二阶段:编译(Compile)

文件后缀.i.s做的事情:语法检查、语义分析,把 C 代码翻译成汇编语言 特点:变成汇编指令,还不是机器能直接跑的

第三阶段:汇编(Assemble)

文件后缀.s.o(目标文件)做的事情:把汇编代码翻译成二进制机器码 特点 :已经是机器码,但不能独立运行,缺少函数、库的地址

第四阶段:链接(Link)

文件后缀.o.exe / elf(可执行文件)做的事情:

  1. 合并多个目标文件
  2. 链接系统库(比如 printf、scanf)
  3. 分配内存地址、解析符号特点:完成后,就是可以直接运行的程序了

连接器的作用

编译器生成 .o 文件,链接器把它们拼成最终的 .bin 文件,并决定每个函数、变量在内存里的位置。ELF 是 Linux/Unix 系统下,可执行文件、目标文件、共享库的标准格式。你平时编译出来的 .o 目标文件、可执行程序、.so 动态库,本质上都是 ELF 格式。它包含了程序运行所需的全部信息:

  • 机器指令代码(.text 段)
  • 初始化数据(.data 段)
  • 未初始化数据(.bss 段)
  • 符号表、重定位信息、调试信息等

十二、在 Linux 下,程序从编译到运行的完整流程都离不开 ELF:

  • 编译阶段:编译器生成 .o 目标文件,这是 ELF 格式。
  • 链接阶段:链接器把多个 .o 文件整合成一个 ELF 格式的可执行文件。
  • 运行阶段:操作系统内核加载器会解析 ELF 文件头,把代码段、数据段映射到内存,然后跳转到入口点执行。

ELF 就是程序在磁盘上的 "蓝图",它告诉操作系统怎么把程序加载到内存并运行。RT-Thread 内核通过 dlopen/dlsym 加载 ELF 格式的 .so 模块,再通过 symtable 符号表,让模块可以调用内核的 API,实现动态扩展。

相关推荐
odoo中国2 小时前
Odoo 19技术教程 : 如何在 Odoo 19 中创建 Many2one 组件
开发语言·odoo·odoo19·odoo技术·many2one
逻辑驱动的ken2 小时前
Java高频面试考点场景题14
java·开发语言·深度学习·面试·职场和发展·求职招聘·春招
FreakStudio2 小时前
和做工厂系统的印尼老哥,复刻了一套属于 MicroPython 的包管理系统
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
HIZYUAN3 小时前
AG32 MCU Reference Manual(202401008修订版)使用手册
单片机·嵌入式硬件
techdashen3 小时前
Cloudflare 如何把一个大型代理拆成三个小服务来提升可靠性
开发语言·rust
geovindu4 小时前
go: Chain of Responsibility Pattern
开发语言·设计模式·golang·责任链模式
guygg884 小时前
STM32 汉字显示程序(标准外设库版本)
stm32·单片机·嵌入式硬件
十五年专注C++开发4 小时前
WaitingSpinnerWidget: 一个高度可配置的自定义Qt等待加载动画组件
开发语言·c++·qt·waitingspinner
qeen874 小时前
【数据结构】树的基本概念及存储
c语言·数据结构·c++·学习·