该博客仅用来记录学习和工作中遇到的问题和解决方案,另外还包括一些工具便捷使用的方案,仅供参考和学习

Crash case

1. 栈满了(stack over)

a. 增加Task的栈大小
b. 当一个函数被注册在B Task中时(该Task的栈不能修改),可以在该函数中通过消息队列,将数据发送到A Task中(可自定义的Task)处理

2. 同一块buf释放两次

a. 内存分配注意事项:初始化后判空;使用后释放;释放后置空
	char *pm = (char *) malloc(size);
	if (pm == NULL) 内存分配失败 return;
	使用pm
	free(pm);
	pm = NULL;

3. 当Crash 时,但是因为log 会丢失,不能确切定位到Crash地方

a.这种情况很特殊,有时候会有完整的Crash报告,可以根据相关的工具解析出来大概Crash的原因;
b.如果不好定位地方,可以先==拿掉一些可疑函数==,从而粗略确定Crash位置,然后可以在可疑的
地方==提前return==(即不让他跑一段),来确定精细位置;总的来说就是精确定位Crash位
置,==将Crash慢慢的夹出来==

Problem solutions

1. 一个独立的模块想要调其他模块的函数接口

在原方案中,模块B需要调用模块A中的read and write 函数,因此需要包含模块A的头文件
解决方案:为了让模块B更加独立,可以在模块B中建立read 和 write 函数指针,并创建注册函数;
在模块A中调用模块B的注册函数,将模块A中的函数可以在模块B中使用,而保证编译的时候不依赖与模块A。
实现如下。
优点:减少模块之间的耦合
模块B:
typedef void (*read_f)(char* buf, int len);
typedef void (*write_f)(char* buf, int len);
read_f pread = NULL;
write_f pwrite = NULL;
void register_read_fun(void* pfun)
{
	pread = (read_f)pfun;
}
void register_write_fun(void* pfun)
{
	pwrite = (write_f)pfun;
}

void xxxB_task()
{
	if (xxx){
		pread(buf, len);
	}else (xxx){
		pwrite(buf, len);
	}
}
模块A:可以使用include模块B的头文件来调用注册函数,或者使用extern
extern void register_read_fun(void* pfun)
extern void register_write_fun(void* pfun)
void read(char* buf, int len)
{
	//读操作
}
void write(char* buf, int len)
{
	//写操作
}

void xxxA_task()
{
	register_read_fun(read);
	register_write_fun(write);
	while(1){
		//do some thing
	}
}

2. porting layer:

Q:一个烧录程序的功能,可以通过IIC\SPI\UART三种通信方式,未来还可能增加新的通信方式,
为了降低开发成本怎么设计。
A:烧录程序这一段的flow是相同的,仅通信方式不一样,通信中涉及到read,write,
setting等函数,我们可以使用一个函数指针结构体来实现上述功能。如果是C++就更好做了,
先建一个base类,其中有read,write,setting的虚函数,然后通过继承分别去实现不同的通信方式。
优点:扩展性好
通过结构体实现上述方案:
typedef void (*read_f)(char* buf, int len);
typedef void (*write_f)(char* buf, int len);
typedef void (*setting_f)(void* set);
typedef struct {
	read_f pread;
	write_f pwrite;
	setting_f psetting;
} Com;
Com c;
void main_task_init()
{
#ifdef IIC
c = iic函数接口
#enif
#ifdef SPI
c = spi函数接口
#enif
#ifdef UART
c = uart函数接口
#enif	
}
void main()
{
	main_task_init();
	c.psetting(buf, len);
	.....
	c.pread(buf, len);
	....
	c.pwrite(buf, len);
}

如果需要新加通信方式CAN
只需要新建一个文件,实现结构体中的函数,然后在main_task_init加上函数的注册即可

3. Task没有处理消息

1.可能是消息队列爆掉了
2.可能卡在处理某一条消息里面了,退不出来,所以下一条消息也没办法处理

4. 定时器

1.使用POSIX函数timer_create创建定时器(可移植性好)
2.使用epoll,及事件驱动方式,通过timerfd_create创建(适合配合文件系统使用)

5. 一般嵌入式软件架构(仅参考)

一般来说,分为四个层,分别是app层,service 层, kernel 层和drive驱动,
如下是一个host(PC或者手机),app层,service层设计的例子:
1. 从上到下host->app->service:host 发送送数据->app, app 通过中断接
收解析数据并发送->main_task(或者其他task)中,其中根据不同的
消息ID,处理不同的事情,如果需要将数据传输到service层的可以直接
调用service层的handle函数,service层可以保存数据,或者加载数据提供给kernel。
2.从下层到上层service->app->host: service层有一个注册函数,
可以将app的一个函数当作一个用户来注册(因此service还可以支持多用户,
需要一个source id来区分用户),在service处理传递kernel上来的数据,
调用用户的回调函数,回调函数中,根据不同的消息ID,处理不同功能的数据,
然后将结果通过uart的方式传输给host。
3.一般的消息格式typedef {int msg_id; int msg_len; char* msg_data} msg;

6. Android NDK Crash分析(一般是C++写的守护进程,用于对接硬件和HIDL)

一般是分析Crash的Callstack 或者是Tombstone;
分析流程和使用方法可参考:https://zhuanlan.zhihu.com/p/52270464

7. 嵌入式中常见的异常情况

1.assert : 断言, assert(0)
2.NMI Fault : 非屏蔽中断,可以先关闭中断,然后调用while(1); 进入死循环,等3-4分钟会触发该Crash
3.HARD Fault : 硬件故障,assert(0)也属于这一类
4.MEM Fault : 内存访问错误, char* p = NULL; *p=0; 可触发
5.USAGE Fault : 使用错误,比如除零;当时在这卡了好久,没有实现这种Crash; 
如果未出现这种Crash可以考虑以下两种情况:
	a.被系统优化了,我们可以在变量前面加volatile 修饰
	b.系统没有打开除0的异常处理,需要在系统初始化的时候去设置

8. A 和 B 进程 sync setting

问题描述: A 属于上层,B 属于下层, 正常情况下是B 先运行起来,然后A运行起来,A 就可以对 B 做一些设置;如果 A 先起来呢, 
A 怎么去连接B,怎么sync setting to B???
解答:假设A, B是通过本地套接字连接的,B 作为服务器,A 作为客户端;因此 A 会一直尝试连接B, 如果失败,延时一会,
继续尝试连接,直到连接成功为止;怎么sync setting 呢? 我们可以在A 连接成功时下发一条msg (比如 sync_setting_req,
或者告诉B 注册A成功的消息) to B, B会回一条msg (sync_setting_cnf), 然后A 收到msg 后将相关的setting sync to B。
 (可以考虑一下为什么不在连接成功的时候直接sync setting 呢,而是先发一条msg to B??, 
 可能是要分情况,有些setting 是B sync to A 的,这样子做就不ok; 单纯的A sync setting to B is ok )

9. 本地套接字(sockaddr_un)UDP 服务器和客户端开发

问题描述:该通信方式主要用于进程间通信(IPC),比如A进程作为Client,需要发数据到B进程(Server)进行数据处理,
然后返回结果给A。在这个过程中,遇到了B->A发送数据失败,显示Transport endpoint is not connected,==意思是连接失败==。
解决方案:在大多数UDP通信案例中,服务端会建立一个socket并调用bind绑定一个sockaddr,然而客户端仅需创建一个socket, 
然后发送数据给服务器即可(sendto中需要带上服务器的sockaddr),在服务器收到客户端的数据时,同时也会收到客户端的sockadr,
服务器直接向客户端的sockaddr sendto data就行。但是在本地套接字中向client发送数据失败。==最后修改方案,
在client中定义一个本地套接字path,并绑定到client 的socketFd,这样服务器就能发送数据给客户端了。==
怀疑只能通过IP+port的方式,Client 不用绑定一个端口或者path,系统会自动生成一个,但是本地套接字不会

10. 数据类型转换问题(有符号和无符号的数据范围)

问题描述:在发送数据时,明明发送的是一个uint16 dataA, 但是收到的是一个很大的数(使用get_short()获取数据,
返回的是一个short类型的),在计算中int count = get_short(p) / 96, 当dataA = 400*96 = 38400,
时会得到一个很大的count,导致计算出错
原因分析:当dataA = 38400时,通过get_short(p)得到的是一个负数(因为short 范围是-32768 to 32767, 然而dataA超范围了)
解决方案:uint16 dataB = get_short(p); 通过这一步自动转换一下(有符号转成无符号的)

11. C/C++中怎么拼接文件路径和文件名

有时候在配置文件中,定义了文件名和文件路径,但是怎么组合上去呢?可以在使用的时候定义一个函数来组合来模仿Pyhton中的join函数。另外一个好的办法是使用宏定义,它会自动拼接,实现如下:
#define PATH /path/log/
#define FILE log.txt
#define MERGER_PATH PATH FILE

12. 频繁发送两个切换指令,使最后状态为最后指令的状态

问题描述:
如果有两个指令,一个是lock power, 一个是unlock power, 两个操作都比较耗时(其实不耗时下面的问题就可忽略不计);上层发送两个消息给下层处理,当速度很慢时,可以正常工作;但是当两个消息发送频率很快时,就会出现问题:比如,先lock,马上发送unlock,这时lock还没完成,unlock的指令就下来,所以最后的结果可能是lock,还有一种情况就是连续发送同一个,下层会一直处理同一个消息,增加负载。
最后的需求是:
当快速切换状态后,最后的实际状态会是最后设置的状态。
解决办法:
正常使用流程:上层发送一个lock, 下层收到这个消息后,将status = processing,然后去处理lock,并设置回调函数lockCb,成功后通知上层。发送一个unlock, 下层将status = processing(如果这个操作很快,则可以直接为unlock,并且不需要回调函数),设置回调函数unlockCb,成功后通知上层。
怎么解决:一共有三个状态,lock, unlock, processing; 下层每次收到消息后,使用一个desireStatus来保存期望的状态。在lockCb中,来判断期望状态和lock是否相等,如果相等则通知上层,不等则进行unlock操作。在unlcokCb中,同理,来判断期望状态和unlock是否相等,如果相等则通知上层,不等则进行lock操作。
另外还有一点,防止处理重复的上层指令,在收到lock msg后,if status != unlock: return; 在收到unlock msg后,if status != lock: return;

伪代码:
status = unlock;
Case : LOCK {
	desireStatus = lock;
	if (status != unlock) return;
	status = processing;
	handleLock(lockCb);
}
Case : UNLOCK {
	desireStatus = unlock;
	if (status != lock) return;
	status = processing;
	handleUnlock(unlockCb);
}

lockCb() {
	if (desireStatus == lock) {
		status = lock;
		notifyUpon(lock);
	} else if (desireStatus == unlock) {
		status = processing;
		handleUnlock(unlockCb);
	}
}

unlockCb {
	if (desireStatus == unlock) {
		status = unlock;
		notifyUpon(unlock);
	} else if (desireStatus == lock) {
		status = processing;
		handlelock(lockCb);
	}
}


13. 抽象套接字中服务端和客户端中的地址长度计算

在本地的进程通信时,为什么要使用抽象套接字呢?因为不依赖文件系统,他以空字符开头“\0”, 当最后一个引用关闭时自动消失

  1. 抽象套接字服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_NAME "\0abstract_socket_example"
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_un addr;
    char buffer[BUFFER_SIZE];
    
    // 创建UNIX域套接字
    if ((server_fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    
    // 设置地址结构
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path + 1, SOCKET_NAME + 1, sizeof(addr.sun_path) - 2); // 跳过第一个空字符
    
    // 计算地址长度
    socklen_t addr_len = sizeof(addr.sun_family) + 1 + strlen(SOCKET_NAME + 1);
    
    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&addr, addr_len) == -1) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    // 开始监听
    if (listen(server_fd, 5) == -1) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    printf("Server is listening...\n");
    
    // 接受客户端连接
    if ((client_fd = accept(server_fd, NULL, NULL)) == -1) {
        perror("accept");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    printf("Client connected.\n");
    
    // 读取客户端数据
    ssize_t num_read = read(client_fd, buffer, BUFFER_SIZE);
    if (num_read == -1) {
        perror("read");
    } else {
        printf("Received: %.*s\n", (int)num_read, buffer);
    }
    
    // 关闭连接
    close(client_fd);
    close(server_fd);
    
    return 0;
}

2.抽象套接字客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_NAME "\0abstract_socket_example"
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_un addr;
    const char *message = "Hello from client!";
    
    // 创建UNIX域套接字
    if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    
    // 设置地址结构
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path + 1, SOCKET_NAME + 1, sizeof(addr.sun_path) - 2); // 跳过第一个空字符
    
    // 计算地址长度
    socklen_t addr_len = sizeof(addr.sun_family) + 1 + strlen(SOCKET_NAME + 1);
    
    // 连接服务器
    if (connect(sockfd, (struct sockaddr *)&addr, addr_len) == -1) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    printf("Connected to server.\n");
    
    // 发送数据
    if (write(sockfd, message, strlen(message)) == -1) {
        perror("write");
    }
    
    // 关闭连接
    close(sockfd);
    
    return 0;
}

3.解释和注意事项

struct sockaddr_un {
    sa_family_t sun_family;     /* AF_UNIX */
    char        sun_path[108];  /* 路径名 */
};

a.服务端和客户端的addr_len计算方式必须一样,不然会链接不上(必须使用计算出的正确地址长度,而不是简单的 sizeof(struct sockaddr_un));
b.sun_family 必须设置为 AF_UNIX;
c.sun_path 的第一个字节是空字符(‘\0’),后面跟着抽象名称;
d.整个 sun_path 不表示实际文件系统路径;
e.长度计算方式地址长度 = sizeof(sun_family) + 1(开头的空字符) + strlen(抽象名称部分);
f.sizeof(addr.sun_path) - 2 保留空间给开头的’\0’和结尾的’\0’;

14. 在解决发现调试Android C/C++ AIDL层的程序卡住的问题

现象:当程序运行时,由于某些锁之类的,导致程序不能往下执行,程序就会卡住,如果log 不是很足,很难查出在哪里卡住了。
解决办法:ps -e | grep exe_name, 找出卡住程序的pid,然后kill -6 pid, adb log 中会出现当时的堆栈信息,进而检查程序卡在什么位置;如果堆栈信息比较少,可以看/data/tombstone.
Kill 的使用方法:
kill [选项] …
kill -9 1234 -> SIGKILL 强制终止信号 (不可捕获或忽略)
kill 1234 -> SIGTERM 优雅终止信号 (默认信号)
kill -6 1234 -> 生成核心转储文件用于事后分析,assert(0)即会触发该信号
killall exe.name -> 按进程名发送信号

Good tool solutions

1. 在win10上学习Linux

具体的Ubuntu 24.04 LTS安装步骤可参考这里

需求来源:如果你使用的是win10系统,但是想进行Liunx开发或者学习。我们可以选择安装虚拟机,
或者安装双系统(win+Linux),相比与后者,前者肯定是较优方案;但是在win10,
可以在Microsoft stroe 下载 Ubuntu 24.04 LTS,目前我认为是最优方案。
如下是安装步骤以便使用:
1.去Microsoft stroe 下载 Ubuntu 24.04 LTS, 并根据他的安装提示(需要设置一些东西),
然后重启,之后就可以使用linux的黑窗命令行了,是不是很简单;
2.安装Vscode, 安装后下载WLS插件(点击左下角的><符号可选择连接), 这样我们就可以以Vscode为编辑器,
进行后续工作了,非常方便,既可以编辑又可以输入命令,还包含了许多插件。
重要的是就不用面对黑框Terminal了,真好。
3.在开发中遇到什么工具没有,就下载,比如make, gdb, g++, gcc等,下载命令,敲一下他会有提示和建议
4.怎么优雅便捷的使用就需要自己慢慢探索了。

2. CSDN 上的技能树(意外发现的宝藏资料)

3. 时间转换工具

Tool link
功能:可用于GPS时间,UTC时间,Unix时间戳等的相互转换

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐