C/C++编程-分层模块化-数据交互

zchs_protocol
vincepipegrab

对于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_adrive_bdrive_c
三个下层托起一个上层
或者
APP_AAPP_BAPP_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,一起由子对象继承。

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值