嵌入式中常见的问题锦集
【代码】嵌入式中常见的问题锦集。
目录
-
- Crash case
- Problem solutions
-
- 1. 一个独立的模块想要调其他模块的函数接口
- 2. porting layer:
- 3. Task没有处理消息
- 4. 定时器
- 5. 一般嵌入式软件架构(仅参考)
- 6. Android NDK Crash分析(一般是C++写的守护进程,用于对接硬件和HIDL)
- 7. 嵌入式中常见的异常情况
- 8. A 和 B 进程 sync setting
- 9. 本地套接字(sockaddr_un)UDP 服务器和客户端开发
- 10. 数据类型转换问题(有符号和无符号的数据范围)
- 11. C/C++中怎么拼接文件路径和文件名
- 12. 频繁发送两个切换指令,使最后状态为最后指令的状态
- 13. 抽象套接字中服务端和客户端中的地址长度计算
- 14. 在解决发现调试Android C/C++ AIDL层的程序卡住的问题
- Good tool solutions
该博客仅用来记录学习和工作中遇到的问题和解决方案,另外还包括一些工具便捷使用的方案,仅供参考和学习
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”, 当最后一个引用关闭时自动消失
- 抽象套接字服务端
#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
需求来源:如果你使用的是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 上的技能树(意外发现的宝藏资料)
- 网页直达
- 使用git开发和管理代码,这里讲的通俗易懂,非常实用
- 另外还有linux 相关的入门和进阶教程,非常值得学习
3. 时间转换工具
Tool link
功能:可用于GPS时间,UTC时间,Unix时间戳等的相互转换
更多推荐
所有评论(0)