一、前言 & 本期目标

上一篇我们学习了嵌入式必备的有 / 无符号类型、位运算、宏、const/volatile、函数指针、标准 GPIO 框架,完成了从入门 C 到嵌入式工程 C 的思维转换。

在实际单片机项目中:串口收发、CAN 总线、传感器数据采集、协议解析、任务状态管理,都会遇到批量临时数据存储、动态数据管理场景。普通固定数组长度死板、空间浪费,这时就必须用到:高级指针 + 链表

本期继续深耕嵌入式专用 C 语法,全程不做 PC 娱乐案例,所有代码、用法、场景均对标 51/STM32、FreeRTOS、通信缓冲区、设备链表

本期学习目标

  1. 吃透结构体指针(嵌入式使用频率最高的指针);
  2. 分清指针数组、数组指针,解决硬件参数表、ID 列表开发问题;
  3. 入门多级指针,看懂复杂工程传参;
  4. 掌握单向链表基础:节点定义、增删查改,用于设备管理;
  5. 实战 环形缓冲区(FIFO 队列)—— 单片机串口 / 通信标配;
  6. 全部案例落地硬件场景,养成嵌入式标准编码习惯。

前置基础:结构体、基础指针、函数、位运算、宏定义


第一部分:嵌入式高频 —— 结构体指针

结构体是外设、参数、设备的载体,结构体指针是工程传参、访问成员的主流写法

1. 基础回顾与指针访问成员

普通结构体用 . 访问成员;结构体指针必须用 -> 箭头访问成员(嵌入式代码随处可见)。

// 统一嵌入式标准类型
typedef unsigned char  u8;
typedef unsigned short u16;
typedef unsigned int   u32;

// 模拟一个外设结构体:简易传感器
typedef struct
{
    u16 addr;    // 设备地址
    u16 data;    // 采集数据
    u8  status;  // 设备状态
}Sensor_Def;

int main(void)
{
    Sensor_Def sen1;
    Sensor_Def *p_sen = &sen1;  // 结构体指针指向实体

    // 指针访问成员:必须使用 ->
    p_sen->addr = 0x01;
    p_sen->data = 1024;
    p_sen->status = 1;

    return 0;
}

2. 结构体指针做函数参数(工程标准写法)

嵌入式函数传递结构体,一律传指针,不传整个结构体。原因:

  1. 结构体数据量大,传值会拷贝整份数据,浪费栈空间、拖慢速度
  2. 传指针直接操作原数据,效率极高,是单片机硬性规范。
#include <stdio.h>

typedef unsigned char  u8;
typedef unsigned short u16;

typedef struct
{
    u16 addr;
    u16 data;
    u8  status;
}Sensor_Def;

// 函数接收结构体指针
void Sensor_SetData(Sensor_Def *p_sen, u16 val)
{
    if(p_sen == NULL)  // 空指针判断,嵌入式必写防护
    {
        return;
    }
    p_sen->data = val;
}

int main(void)
{
    Sensor_Def sen;
    Sensor_SetData(&sen, 2048);

    return 0;
}

嵌入式铁律:大型结构体、外设参数、设备信息,函数传参只传指针,并增加空指针判断防崩溃。


第二部分:指针数组 vs 数组指针(硬件查表必备)

这一组概念极易混淆,在硬件 ID 表、命令表、状态表、协议列表中大量使用。

1. 指针数组:存放多个指针(硬件地址 / ID 表常用)

定义:类型 *数组名[]本质:一个数组,里面每一个元素都是指针

嵌入式场景:多设备地址表、多字符串提示、多命令 ID。

#include <stdio.h>

// 指针数组:存放多个字符串(提示信息表)
const char *MsgTable[] =
{
    "Device Ready",
    "Data Receive",
    "Error Occur",
    "System Reset"
};

int main(void)
{
    // 遍历查表
    printf("%s\n", MsgTable[0]);
    printf("%s\n", MsgTable[1]);

    return 0;
}

搭配 const 修饰,表格存入 Flash,不占用 RAM,单片机最优写法。

2. 数组指针:指向整个数组的指针(底层硬件偏移、批量寄存器)

定义:类型 (*指针名)[长度]嵌入式场景:寄存器块、连续内存区域、批量数据块操作。

typedef unsigned char u8;

u8 buf[8] = {1,2,3,4,5,6,7,8};
u8 (*p_buf)[8] = &buf;  // 数组指针,指向一整个数组

快速区分口诀(记牢不踩坑)

  • char *arr[]指针数组:数组里装指针(查表、列表用)
  • char (*arr)[]数组指针:指针指向数组(整块内存操作)

第三部分:多级指针入门(看懂复杂工程传参)

多级指针 = 指针的指针,嵌入式工程、二级函数、动态传参偶尔会遇到,不用深挖,看懂即可。

基础示例

typedef unsigned char u8;

void Test(u8 **p)
{
    **p = 0x55;
}

int main(void)
{
    u8 val = 0;
    u8 *p = &val;
    Test(&p);   // 传递指针地址

    return 0;
}

应用场景:函数内部修改外部指针本身的指向、动态缓存分配、二级回调,STM32 标准库、第三方协议库中经常出现。


第四部分:单向链表(设备 / 节点管理)

数组长度固定,链表可以动态增减节点,适合:多设备挂载、动态传感器、多路外设管理、离线设备列表。

1. 链表节点定义(嵌入式标准节点)

每个节点 = 设备数据 + 下一个节点指针

typedef unsigned char  u8;
typedef unsigned short u16;

// 链表节点:模拟一个从设备
typedef struct Node
{
    u8  dev_id;     // 设备ID
    u16 dev_data;   // 设备数据
    struct Node *next; // 指向下一个节点
}Node_TypeDef;

2. 基础操作:创建节点、尾插、遍历

完整极简嵌入式链表 Demo:

#include <stdio.h>
#include <stdlib.h>   // malloc / free

typedef unsigned char  u8;
typedef unsigned short u16;

typedef struct Node
{
    u8  dev_id;
    u16 dev_data;
    struct Node *next;
}Node_TypeDef;

// 链表头节点
Node_TypeDef *head = NULL;

// 尾插法:新增设备节点
void Node_Add(u8 id, u16 data)
{
    Node_TypeDef *new_node = (Node_TypeDef *)malloc(sizeof(Node_TypeDef));
    if(new_node == NULL)
    {
        return;
    }
    new_node->dev_id = id;
    new_node->dev_data = data;
    new_node->next = NULL;

    if(head == NULL)
    {
        head = new_node;
    }
    else
    {
        Node_TypeDef *p = head;
        while(p->next != NULL)
        {
            p = p->next;
        }
        p->next = new_node;
    }
}

// 遍历所有设备
void Node_Scan(void)
{
    Node_TypeDef *p = head;
    while(p != NULL)
    {
        printf("ID:%d  Data:%d\n", p->dev_id, p->dev_data);
        p = p->next;
    }
}

int main(void)
{
    Node_Add(0x01, 100);
    Node_Add(0x02, 200);
    Node_Add(0x03, 300);

    Node_Scan();
    return 0;
}

嵌入式使用说明

  1. malloc 动态内存:部分带操作系统 (FreeRTOS/RT-Thread) 单片机常用;
  2. 若无操作系统、裸机单片机,一般改用静态链表(数组模拟链表),避免动态内存碎片;
  3. 用途:多路传感器、多个从机、多路 IO 设备统一管理。

第五部分:本期终极实战 —— 环形缓冲区 FIFO(单片机通信核心)

环形缓冲区(循环队列)是嵌入式开发必掌握项目:串口收发、CAN、网口、传感器数据流、UART 缓存全部基于它实现。

特点:

  • 先进先出 FIFO
  • 循环复用内存,不移动数据,效率极高
  • 固定数组实现,裸机 / RTOS 通用

完整可运行代码(标准环形 FIFO)

#include <stdio.h>
#include <string.h>

// 嵌入式标准类型
typedef unsigned char u8;
typedef unsigned int  u32;

// FIFO 缓存大小
#define FIFO_BUF_LEN 64

// 环形缓冲区结构体
typedef struct
{
    u8 buf[FIFO_BUF_LEN];  // 缓存数组
    u32 wr_ptr;            // 写指针
    u32 rd_ptr;            // 读指针
}FIFO_TypeDef;

// 缓冲区初始化
void FIFO_Init(FIFO_TypeDef *fifo)
{
    if(fifo == NULL) return;
    memset(fifo->buf, 0, FIFO_BUF_LEN);
    fifo->wr_ptr = 0;
    fifo->rd_ptr = 0;
}

// 判断缓冲区是否满
u8 FIFO_IsFull(FIFO_TypeDef *fifo)
{
    return ((fifo->wr_ptr + 1) % FIFO_BUF_LEN) == fifo->rd_ptr;
}

// 判断缓冲区是否空
u8 FIFO_IsEmpty(FIFO_TypeDef *fifo)
{
    return fifo->wr_ptr == fifo->rd_ptr;
}

// 写入1字节数据
u8 FIFO_Write(FIFO_TypeDef *fifo, u8 dat)
{
    if(FIFO_IsFull(fifo))
    {
        return 0; // 已满,写入失败
    }
    fifo->buf[fifo->wr_ptr] = dat;
    fifo->wr_ptr = (fifo->wr_ptr + 1) % FIFO_BUF_LEN;
    return 1;
}

// 读取1字节数据
u8 FIFO_Read(FIFO_TypeDef *fifo, u8 *p_dat)
{
    if(FIFO_IsEmpty(fifo) || p_dat == NULL)
    {
        return 0; // 为空,读取失败
    }
    *p_dat = fifo->buf[fifo->rd_ptr];
    fifo->rd_ptr = (fifo->rd_ptr + 1) % FIFO_BUF_LEN;
    return 1;
}

// 测试主函数
int main(void)
{
    FIFO_TypeDef fifo;
    u8 i, tmp;

    FIFO_Init(&fifo);

    // 连续写入数据
    for(i = 0; i < 10; i++)
    {
        FIFO_Write(&fifo, i);
    }

    // 循环读出数据
    while(FIFO_Read(&fifo, &tmp))
    {
        printf("%d ", tmp);
    }

    return 0;
}

工程应用场景

  1. 串口接收缓存:中断里写 FIFO,主循环读 FIFO,中断极简、系统稳定
  2. CAN 总线数据缓存;
  3. 传感器连续采样数据流;
  4. FreeRTOS 消息队列底层原型。

第六部分:嵌入式 C 指针 & 链表 高频坑点总结

  1. 结构体指针必须用 ->,普通结构体用 .,不要混用;
  2. 函数接收指针务必做空指针判断,单片机空指针直接死机;
  3. 传结构体优先传指针,禁止整结构体传值,节省栈空间;
  4. 指针数组 / 数组指针 书写格式不要写错,查表、寄存器操作极易出错;
  5. 裸机单片机慎用 malloc/free,容易产生内存碎片,优先静态数组 / 静态链表
  6. 环形缓冲区读写指针判断逻辑不要写反,避免数据丢失、重复读取。

第七部分:本篇总结

  1. 掌握结构体指针,看懂并编写嵌入式标准外设函数;
  2. 分清指针数组、数组指针,搞定硬件查表、ID 列表、字符串列表;
  3. 理解多级指针,能阅读复杂第三方库、标准库代码;
  4. 学会单向链表,实现多设备、动态节点管理;
  5. 吃透 环形 FIFO 缓冲区,掌握串口 / CAN 通信底层核心;

至此,嵌入式 C 语言指针体系全部打通。从基础指针 → 函数指针 → 结构体指针 → 链表 → 队列,覆盖 90% 单片机 C 指针用法。


下期预告(继续嵌入式路线)

下一期主题:

嵌入式 C 高级专题:多文件编程 + 工程模块化 + 条件编译 + 头文件规范内容:

  1. 头文件 .h / 源文件 .c 分工规范
  2. 防止头文件重复包含 #ifndef / #define / #endif
  3. 条件编译 #ifdef(版本裁剪、硬件适配)
  4. 多文件工程拆分(GPIO、UART、KEY 分文件管理)
  5. 完整单片机分层工程模板(驱动层、应用层分离)

学完即可搭建标准单片机工程架构,正式进入项目开发阶段。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐