模块间的数据交互
zchs_protocol | ||
vince | pipe | grab |
对于zchs_protocol协议模块和下面的三个运动部件模块之间的交互问题。
实习方式
- 变量
无系统:跨模块的全局变量
有系统:信号量等
- 函数接口
方式选择优缺点说明
关于变量
- 无系统
例如,在zchs_protocol中定义serial_send_state:0没有数据发送,1有数据发送。
需要分别在三个运动部件模块中分别置位此标志变量,然后在zchs_protocol中实现发送函数(检测此标志,然后发送)。
但是这这其中存在的问题是,如果vince模块已经将serial_send_state置1,有vince的数据要发送。那么又有pipe或者grab模块也要置位此位,要申请串口发送数据。那么zchs_protocol就不知道要发送谁的了。说白了,就是需要明确三个底层模块的发送顺序。
发送顺序又有两种实现方式:变量和逻辑控制
变量最简单的就是,每个运动模块都在自己模块内置位,清位serial_send_state。每次置位serial_send_state前,都检测是否为1,如果为1,则放弃此次发送,等待下一次轮询到自己模块有没有数据要发送。 - 有系统
利用信号量,实现上面逻辑业务逻辑
关于函数接口
在zchs_protocol中定义一个serial_send()接口函数,由三个运动部件调用。
但是一样面对,不知道vince调用此函数的时候,pipe和grab有没有调用。这样的问题。
同样需要标志变量或者逻辑控制,实现**“发送接口”**这个共享资源问题。
其实,裸机没有任务中断调度,已经通过逻辑控制解决了这个问题。即,不可能vince在通过zchs_protocol发送数据的时候,pipe和grab也有数据同时申请发送。
但是,由系统的时候,就需要依旧需要信号量去解决这个问题。
综上,对于变量,裸机和系统都适用。
对于接口函数,裸机适用,系统不适用。
所以,变量实现方式更占优势。
优势:适用普遍,有利移植
zchs_protocol uint8_t s_state=0; if(s_state==1) { serial_send(); s_state=0; } |
vince if(v_sate==1) { if(s_state!=1) {s_state=1;v_state=0;} } |
pipe if(p_sate==1) { if(s_state!=1) {s_state=1;p_state=0;} } |
grab if(g_sate==1) { if(s_state!=1) {s_state=1;g_state=0;} } |
zchs_protocol
poll()
{
check_sub_send();
switch(state)
{
ready:break;
recive:break;
send:break;
idle:break;
}
}
上面的描述虽然有些道理,但是经过一些日子的磨炼,现在又感觉有些肤浅。这是有了新的感悟。现描述如下:
模块的堆叠
我们经常在一些让人崇拜的官网上看到下面这样的软件图解:
APP | ||
drive_a | drive_b | drive_c |
APP_A | APP_B | APP_C |
drive |
这是软件的整体划分,由下而上的堆叠,这个我自然很容易了解了,其作用也是一样很容易理解了。但是,若想切身的体会却是不容易。应该还是要去大公司工作,并且有着坚实的基础,才会有着切身的体会。
体会就是:即使上层领导根据项目需求,划分好模块了,那么又如何保证软件的书写、构造完美遵循这个堆叠结构呢?
这里面有大量的工作。我技术不够好、层次不够高,也是说不好的。但是却有着下一些切身体会。
1,模块之间的交互方式:数据交互、函数接口、全局变量。
这些实现方式,就像数学中的“加减乘除”是基本的要素,需要我们自己的搜寻、借鉴。有哪些方式实现、各个实现方式能达到的效果、其优缺点又是什么都要自己平时一一积累总结。就是模拟电路中,运放电路的基本电路”加法、减法、乘法、除法、积分、微分、vi、iv、低通、高通、带通、跟随、锁相环、、、”等一样。这都是你以后设计电路的基础电路,有了他们,就是数学中你学会了加减乘除,才能去用它们解决各种问题,他们是基础啊!这个基础知识不行,那还谈什么。也像画家手中的“笔、纸、赤、橙、黄、绿、金、蓝、紫墨水”,有了它们,你才能做画,是一样的道理。
然而市面上的教材,却少有提及这个的,教这个的。其实,我一直认为最好的教材,莫过于汇聚了各个高手的操作系统源码。所以,我把整理这些基础素材的来源,总是分为“带系统”和“不带系统”两种实现方式。
带系统的模块间交互方式:
其实网上都有。这里只是告诉大家把这些思想联系起来。
数据交互方式:队列、邮箱。
函数接口:注册(操作集的指针赋值)。
就是rt_thread和Linux中驱动架构中的操作集的函数接口赋值。
struct file_operation fops =
{
.open = xxx_open;
.close = xxx_close;
.read = xxx_read;
.write = xxx_write;
.probe = xxx_probe;
};
全局变量:信号量,互斥锁、事件。
不带系统的模块间交互方式:
函数接口:
在“model_a.c”的“model_a.h”中声明对外的
void a_open(void);
void a_close(void);
void a_read(void);
…
或者采用extern关键字修饰,在合适的地方声明。
者两种方式,前者对所有上层模块开放,并且include之后夹带其他通用对外开放信息。所以适合带有common属性。而extern则只需要在需要的上层文件中声明就可,并且不带有.h文件中的其他对外common属性。
对于全局变量和数据交互,也是一样考虑。
2,模块堆叠需要遵守的一些规则和考虑
- 机制和策略(即“输入”和“输出”)
- 认清模块在软件栈中的位置-
- 向下依赖, 不要向上依赖
- 避免同级依赖
两种在上层调用底层API的方式
由于在C语言中,上层挂载底层API都是通过函数指针的方式实现。而,函数指针又要求形参的类型必须一致(void* 除外)。
/* 底层 */
void (write)(uart_object *bus, const uint8_t *buf, uint16_t len);
void (read)(uart_object *bus, uint8_t *buf, uint16_t len);
/* 上层 */
int (write)(bus_object *bus, const uint8_t *buf, uint16_t len);
int (read)(bus_object *bus, uint8_t *buf, uint16_t len);
1. 这里由于bus_object 和 uart_object 不兼容,所以无法实现挂载。
1, 类Linux
众所周知,linux所有的接口驱动都是文件形式。这里以串口为例:
#include <stdio.h>
#include <stdlib.h>
// 文件操作结构体
typedef struct FileOperations {
FILE *(*openFile)(const char *fileName, const char *mode); // 打开文件函数指针
size_t (*readData)(FILE *file, void *buffer, size_t size); // 读取数据函数指针
size_t (*writeData)(FILE *file, const void *buffer, size_t size); // 写入数据函数指针
int (*closeFile)(FILE *file); // 关闭文件函数指针
} FileOperations;
// 打开文件
FILE *openFile(const char *fileName, const char *mode) {
return fopen(fileName, mode);
}
// 读取数据
size_t readData(FILE *file, void *buffer, size_t size) {
return fread(buffer, 1, size, file);
}
// 写入数据
size_t writeData(FILE *file, const void *buffer, size_t size) {
return fwrite(buffer, 1, size, file);
}
// 关闭文件
int closeFile(FILE *file) {
return fclose(file);
}
// 初始化文件操作结构体
FileOperations initFileOperations() {
FileOperations fileOps = {
.openFile = openFile,
.readData = readData,
.writeData = writeData,
.closeFile = closeFile
};
return fileOps;
}
int main() {
FileOperations fileOps = initFileOperations(); // 初始化文件操作对象
// 打开文件用于写入
FILE *fpWrite = fileOps.openFile("example.txt", "w");
if (fpWrite == NULL) {
printf("无法打开文件进行写入\n");
return 1;
}
// 写入数据
const char *dataToWrite = "这是要写入文件的数据";
size_t bytesWritten = fileOps.writeData(fpWrite, dataToWrite, strlen(dataToWrite));
printf("写入 %zu 字节数据到文件\n", bytesWritten);
// 关闭写入文件
if (fileOps.closeFile(fpWrite)!= 0) {
printf("关闭写入文件时发生错误\n");
return 1;
}
// 打开文件用于读取
FILE *fpRead = fileOps.openFile("example.txt", "r");
if (fpRead == NULL) {
printf("无法打开文件进行读取\n");
return 1;
}
// 读取数据到缓冲区
char buffer[100];
size_t bytesRead = fileOps.readData(fpRead, buffer, sizeof(buffer) - 1);
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // 添加字符串结束符
printf("读取到的数据: %s\n", buffer);
} else if (bytesRead == 0) {
printf("已到达文件末尾\n");
} else {
printf("读取文件时发生错误\n");
}
// 关闭读取文件
if (fileOps.closeFile(fpRead)!= 0) {
printf("关闭读取文件时发生错误\n");
return 1;
}
return 0;
}
可见,不论是spi 还是iic 还是uart 都可采用FILE 文件读写,他们上下层公用一种类型格式。所以,我借鉴其形式,将所有通信接口都用void * 来弄。如下:
/* 底层 */
int (write)(uart_object *bus, const uint8_t *buf, uint16_t len);
int (read)(uart_object *bus, uint8_t *buf, uint16_t len);
/* 上层 */
enum PRO_BUS{
PRO_BUS_UART = 0,
PRO_BUS_END,
}
struct pro_ops{
int (write)(void *bus, const uint8_t *buf, uint16_t len);
int (read)(void *bus, uint8_t *buf, uint16_t len);
}
struct pro_bus_object{
enum PRO_BUS kind;
void *handle;
struct pro_ops ops;
}
struct pro_object{
char name[15];
......
struct pro_bus_object bus;
}
static struct pro_bus_object protocal = {
};
int pro_init(void)
{
/* 获取端口句柄 - uart\spi\iic */
protocal.bus.kind = PRO_BUS_UART;
protocal.bus.handle = (void *)huart1;
protocal.bus.ops = {
.write = int (write)(uart_object *bus, const uint8_t *buf, uint16_t len); /* 这里待探究 */
...
}
}
2. 用rt_thread的ops实现方式
每个父对象有自己的ops,一起由子对象继承。