扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

搞嵌入式,谁不想拥有一组拿来就用的实用代码?

10小时前
194
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

上次分享了一波实用代码片段嵌入式 Linux 必知:几个超实用代码小片段,不少朋友留言说想看更多,那今天就再来一波。

这次的内容涉及结构体内存布局、文件读写封装、终端进度条、core dump 调试这几块,都是平时项目里高频用到的东西。废话不多说,直接上菜。

本文代码均基于 Linux + GCC 环境验证。

终端进度条

先来个有意思的——终端进度条。

OTA 升级、固件烧写、批量文件拷贝的时候,光看日志刷屏心里没底,加个进度条一目了然。效果先看:

是不是还挺像回事?实现起来其实也不复杂,核心就是 r 回车符不换行,配合 fflush 刷新输出缓冲区来实现同行刷新。

代码:

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

typedefstruct _progress
{
    int cur_size;
    int sum_size;
}progress_t;

void progress_bar(progress_t *progress_data)
{    
    int percentage = 0;
    int cnt = 0;
    char proc[102]; // 100个字符位 + 最后一个'#' + ''

    memset(proc, '', sizeof(proc));

    percentage = (int)((longlong)progress_data->cur_size * 100 / progress_data->sum_size);
    printf("percentage = %d %%n", percentage);

    if (percentage <= 100)
    {  
        while (cnt <= percentage)
        {
            printf("[%-100s] [%d%%]r", proc, cnt);
            fflush(stdout);  
            proc[cnt] = '#';  
            usleep(100000);
            cnt++;
        }
    }  
    printf("n");
}

int main(int arc, char *argv[])
{
    progress_t progress_test = {0};

    progress_test.cur_size = 65;
    progress_test.sum_size = 100;
    progress_bar(&progress_test);
    
    return0;
}  

运行结果:

快速获取结构体成员大小及偏移量

搞嵌入式的应该都知道,结构体的内存布局、对齐方式这些细节经常要关注——通信协议解析、共享内存操作的时候一不注意就踩坑。

获取结构体成员偏移量,标准库 <stddef.h> 里有个 offsetof 宏可以用,不过我们也可以自己实现一版,顺便搞清楚它背后的原理。

思路很简单:把 0 地址强转成结构体指针,再去取成员的地址,这个地址的值就是偏移量——因为结构体基地址是 0 嘛。获取成员大小同理,对这个"虚拟成员"做 sizeof 就行。

代码:

#include <stdio.h>   

#define  GET_MEMBER_SIZE(type, member)   sizeof(((type*)0)->member)

#define  GET_MEMBER_OFFSET(type, member)  ((size_t)(&(((type*)0)->member)))

typedefstruct _test_struct0
{
    char x;  
    char y; 
    char z;
}test_struct0;

typedefstruct _test_struct1
{
    char a;  
    char c; 
    short b;         
    int d;
    test_struct0 e;
}test_struct1;

int main(int arc, char *argv[])
{
    printf("GET_MEMBER_SIZE(test_struct1, a) = %zun", GET_MEMBER_SIZE(test_struct1, a));
    printf("GET_MEMBER_SIZE(test_struct1, c) = %zun", GET_MEMBER_SIZE(test_struct1, c));
    printf("GET_MEMBER_SIZE(test_struct1, b) = %zun", GET_MEMBER_SIZE(test_struct1, b));
    printf("GET_MEMBER_SIZE(test_struct1, d) = %zun", GET_MEMBER_SIZE(test_struct1, d));
    printf("GET_MEMBER_SIZE(test_struct1, e) = %zun", GET_MEMBER_SIZE(test_struct1, e));
    printf("test_struct1 size = %zun", sizeof(test_struct1));

    printf("GET_MEMBER_OFFSET(a): %zun", GET_MEMBER_OFFSET(test_struct1, a));
    printf("GET_MEMBER_OFFSET(c): %zun", GET_MEMBER_OFFSET(test_struct1, c));
    printf("GET_MEMBER_OFFSET(b): %zun", GET_MEMBER_OFFSET(test_struct1, b));
    printf("GET_MEMBER_OFFSET(d): %zun", GET_MEMBER_OFFSET(test_struct1, d));
    printf("GET_MEMBER_OFFSET(e): %zun", GET_MEMBER_OFFSET(test_struct1, e));

    return0;
}

运行结果:

跑出来的结果可以留意一下偏移量——b 的偏移是 2 而不是紧挨着 c 后面的 1+1=2(呃,这里恰好对上了),但 d 的偏移是 4,这就是内存对齐在起作用。编译器为了让 int 对齐到 4 字节边界,会在 b 后面自动填充。搞通信协议的时候不注意这个,对端解析大概率就乱套了。

文件操作封装

文件读写的代码我们几乎每个项目都要写,什么配置参数存储啊、日志落盘啊、固件数据读写啊,太常见了。与其每次都重新写一遍 fopen/fwrite/fclose 那套,不如封装两个通用函数,拿来直接用:

代码:

#include <stdio.h>   

static int file_opt_write(const char *filename, void *ptr, int size)
{   
    FILE *fp;
    size_t num;

    fp = fopen(filename, "wb");
    if (NULL == fp)
    {
        printf("open %s file error!n", filename);
        return-1;   
    }
    
    num = fwrite(ptr, 1, size, fp);
    if (num != size)
    {
        fclose(fp);
        printf("write %s file error!n", filename);
        return-1;
    } 

    fclose(fp);

    return (int)num;
}

static int file_opt_read(const char *filename, void *ptr, int size)
{
    FILE *fp;
    size_t num;

    fp = fopen(filename, "rb");
    if (NULL == fp)
    {
        printf("open %s file error!n", filename);
        return-1;
    }
    
    num = fread(ptr, 1, size, fp);
    if (num != size)
    {
        fclose(fp);
        printf("read %s file error!n", filename);
        return-1;
    } 
    fclose(fp);

    return (int)num;
}

typedefstruct _test_struct
{
    char a;  
    char c; 
    short b;         
    int d;
}test_struct;

#define FILE_NAME  "./test_file"

int main(int arc, char *argv[])
{
    test_struct write_data = {0};
    write_data.a = 1;
    write_data.b = 2;
    write_data.c = 3;
    write_data.d = 4;
    printf("write_data.a = %dn", write_data.a);
    printf("write_data.b = %dn", write_data.b);
    printf("write_data.c = %dn", write_data.c);
    printf("write_data.d = %dn", write_data.d);
    file_opt_write(FILE_NAME, (test_struct*)&write_data, sizeof(test_struct));

    test_struct read_data = {0};
    file_opt_read(FILE_NAME, (test_struct*)&read_data, sizeof(test_struct));
    printf("read_data.a = %dn", read_data.a);
    printf("read_data.b = %dn", read_data.b);
    printf("read_data.c = %dn", read_data.c);
    printf("read_data.d = %dn", read_data.d);

    return0;
}

这里用 "wb" / "rb" 模式打开文件,就是二进制读写,跟 "w" / "r" 文本模式的区别在于不会对换行符做转换。写结构体数据用二进制模式才是对的,不然在 Windows 上可能会多出 r 来。

运行结果:

后台运行生成 core 文件

程序跑着跑着突然挂了,段错误、非法访问这类问题最头疼——出了现场就没了。这时候如果程序在崩溃时能自动吐出 core 文件,事后再用 gdb 加载分析,就能精准定位到哪行代码出的问题。

代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/resource.h>

#define SHELL_CMD_CONF_CORE_FILE    "echo /var/core-%e-%p-%t > /proc/sys/kernel/core_pattern"
#define SHELL_CMD_DEL_CORE_FILE     "rm -f /var/core*"

static int enable_core_dump(void)
{
    int resource = RLIMIT_CORE;
    struct rlimit rlim;

    rlim.rlim_cur = RLIM_INFINITY;
    rlim.rlim_max = RLIM_INFINITY;

    system(SHELL_CMD_DEL_CORE_FILE);

    if (0 != setrlimit(resource, &rlim))
    {
        printf("setrlimit error!n");
        return-1;
    }

    system(SHELL_CMD_CONF_CORE_FILE);
    printf("core dump enabled, pattern: /var/core-%%e-%%p-%%tn");

    return0;
}

int main(int argc, char **argv)
{
    enable_core_dump();

    printf("==================segmentation fault test==================n");

    // 下面故意触发段错误,仅为演示 core dump 功能
    int *p = NULL;
    *p = 1234;

    return0;
}

core 文件生成后,用 gdb ./your_program /var/core-xxx 加载,bt 命令看调用栈,基本就能锁定崩溃位置了。


好了,以上就是本次分享的几个代码片段,简单汇总一下:

代码片段 适用场景
终端进度条 OTA 升级、固件烧写、批量操作
结构体成员大小/偏移 协议解析、内存布局分析
文件读写封装 配置存储、日志落盘、数据持久化
core dump 使能 崩溃后定位、事后调试

这些代码片段我自己是一直在用的,建议收藏起来丢到自己的工具库里,用到的时候直接拿。

你平时还有哪些压箱底的代码片段?欢迎评论区聊聊,说不定下一期就整理进来了。

期待你的三连支持!

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!