程序框架设计示例2_RTOS
报名了百问网的七天物联网课程通过两天的学习确实收获很多。下面几天我会陆续把每节课程的重点、难点以及自己的理解记录下来。
1. 系统拆分:理论介绍
在《代码大全》第5章中,把程序设计分为这几个层次:
-
第1层:软件系统,就是整个系统、整个程序
-
第2层:分解为子系统或包。比如我们可以拆分为:输入子系统、显示子系统、业务系统
子系统如何实现,要使用面向对象的思想抽象成结构体。
-
第3层:分解为类。在C语言里没有类,可以使用结构体来描述子系统。
-
第4层:分解成子程序:实现那些结构体(结构体中有函数指针)
2. 怎么拆分为子系统
2.1 拆分原则
上一次的裸机是把系统分为按键系统和LED灯系统。各个子系统要尽可能独立。
2.2 按照数据流向分:输入/输出/业务
输入来源很多种:
2.3 根据输入源来细分输入部分
又可以细分为:用户输入、传感器、远程控制等等。
2.4根据输出 细分输出部分
又可以细分为:显示屏、控制各类设备、数据保存等等。
2.5 输入、输出的关系:业务
2.6 驱动和应用分开
在Linux驱动开发中,有一句话:驱动只提供功能,不提供策略。
什么意思呢?就是各司其职,不要越界。
以LCD的使用为例,可以分为3层:
- 驱动程序:
- 提供像素操作的功能
- 但是怎么显示字符、显示多大、在哪显示,这不关我的事
- 库函数/功能函数:
- 提供显示字符、显示图片的功能
- 但是显示什么字符、在哪显示,这不关我的事
- APP:
- 使用库函数来显示字符、显示图片
- 我甚至不需要看驱动程序
2.7 举例
用户小程序来控制开发板上的灯、风扇。
1.在LCD上显示字符A:
三种硬件结构来实现LCD显示。现在我要设计一种显示系统,要能够支持这三种结构。
//构造一个结构体能够支持所有的LCD设备。
Display_dev{
char *name;
int ixres; //reselution x方向上的分辨率
int iyres; //y方向上的分辨率
int iBpp; // 第一种与第三种可以确定一个基地址(Base Adder)
//但是对于第二种,地址是通过IIC访问的,无法直接访问,那如何处理:
//在Cpu中划分出一个内存地址。分配出一块临时内存,可以获得基地址。在刷到上面。
char *FramBuffer;
//对于第二种我们要刷新,因此要定义这个刷新函数。
void (*FlashFB)() ;//第二种情况要临时缓存,刷新到显存中去。
}
首先由四个文件:framebuffer_manager.c/framebuffer_manager.h/mylcd.c/oled.c
//framebuffer_manager.h
//首先构造结构体
typedef struct DisplayDev {
char *name;
int iXres;
int iYres;
int iBpp;
char *Framebuffer; //显存刷新
void (*Init)(struct DisplayDev *d);
void (*FlushFB)(struct DisplayDev *d);
}DisplayDev, *p_DisplayDev;
//framebuffer_manager.c
//管理层
int dev_cnt = 0;
DisplayDev *devs[32]; //显示的指针数组。
//注册函数实现。
void register_display_dev(DisplayDev *dev)
{
devs[dev_cnt] = dev;
dev_cnt++;
}
//初始化函数,然后根据名字来找到要注册的结构体。根据结构体找到各种信息。
void desplay_devs_init(void)
{
oled_init();
}
//用名字取出。
DisplayDev *get_display_dev(char *name)
{
int i = 0;
for (i = 0; i < dev_cnt; i++)
if (strcmp(name, devs[i]->name) == 0)
return devs[i];
return NULL;
}
// 返回值: 0表示被按下, 1表示被松开
static int read_key1(void)
{
static GPIO_PinState pre_key;
GPIO_PinState key;
key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
/* 按键无变化 */
if (key == pre_key)
return -1;
/* 按键发生了变化 */
pre_key = key;
if (key == GPIO_PIN_RESET)
return 0xA | (1<<7);
else
return 0xA;
}
static key k1 = {"k1", 0xA, NULL, read_key1};
void k1_init(void)
{
register_key(&k1);
}
void k1_isr(void)
{
GPIO_PinState key;
key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
/* 按键发生了变化 */
if (key == GPIO_PIN_RESET)
put_buf(0xA | (1<<7));
else
put_buf(0xA) ;
}
//oled.c文件
//
//初始化函数
static void oled_init(DisplayDev *dev)
{
}
//刷新函数
static void oled_flush(DisplayDev *dev)
{
}
static char oled_fb[1024];//128*64/8(1位=8bit) = 1024
static DisplayDev oled = {
"oled", //名字
128, //x方向
64, //y方向
1, //每个像素用1位来表示
oled_fb,
oled_init,//函数
oled_flush,//函数
};
void oled_init(void)
{
//注册在管理层写。
register_display_dev(&oled);
}
文件夹:
./main
./Framebuffer
/framebuffer_manager.c
/framebuffer_manager.h
/mylcd.c
/oled.c
把上面的配置定义完成后,如何去使用呢,写一个main.c文件
//main.c文件
void main(void)
{
DisplayDev *pdev;
int size;
//初始化设备
desplay_devs_init();
//获得设备
pdev = get_display_dev("oled");
//初始化一下获得的设备。
pdev->Init(pdev);
//写数据。
size = pdev->iXres * pdev->iYres * pdev->iBpp / 8;
memset(pdev->Framebuffer, 0, size);
//设置完后我就刷新一下。
pdev->FlushFB(pdev);
}
以上实现了显示设备的驱动与使用,下面设计显示系统。如图所示
硬件方向上:显示设备:精力放在硬件操作上。如何设计信号,存储,传输。
显示系统:精力放在像素上
字体系统:精力放在‘A’___字体。类型。
下面演示显示系统的代码实现:
首先是文件夹安排:
./main
./framebuffer
/framebuffer_manager.c
/framebuffer_manager.h
/mylcd.c
/oled.c
./display
/display.c
//display.c
//这个函数跟硬件没什么关系。
/* color: 0x00RRGGBB */
//设置像素。设置DisplayDev *pdev显示设备的像素,坐标是(x,y),设置成什么颜色。
int SetPixel(DisplayDev *pdev, int x, int y, int color)
{
//首先找到了基地址,然后根据地址确定位置。基地址等于Framebuffer;
char *base = pdev->Framebuffer;
//偏移值是y行*每行占得字节数(像素)+x个像素
//这个位置的前面有这么多的像素 ,每一个像素占据多少位?
//偏移值*iBpp/8
int offset = (y*(pdev->iXres)+x)*pdev->iBpp/8;
//当y坐标为零,x坐标对应0-7是一个字节,8-15。。。
//假设初始坐标是(0,0),一个坐标是(x,y)找到他的offset是多少:
// 首先:y前面从0——(y-1)相当于有y行 。每一行有多少像素呢?y*(pdev->iXres)
//然后第0——(x-1)像素相当于有x列个像素。
//那么在(x,y)坐标之前, 有多少个像素呢,就相当于一个小块的面积,如图所示
//(y*(pdev->iXres)+x)个像素,每个像素占据多少位,要除以8.
char *data;
char *dataw;
int bit = x % 8;
data = &base[offset]; //data这个指针指向base这个基地址,偏移值是offset。
dataw = &base[offset];
//怎么操作这个基地址呢。判断多少位。
switch (pdev->iBpp)
{
//每个像素1位你如何操作
case 1:
{
if (color)//不等于零表示要去点亮他。
data |= (1<<bit);
else
data &= ~(1<<bit);//否则清掉。
break;
}
//每个像素8位你如何操作
case 8:
{
*data = color;
break;
}
//每个像素16位你如何操作
case 16:
{
*dataw = color;
break;
}
}
}
3. 子系统的核心:结构体/类
在Linux开发、Linux APP开发中,类/结构体时核心。
能抽象出这些类/结构体,就体现了你的编程能力。
为什么?
-
面向对象:用结构体来表示一个功能,比如输入、输出、网络传输、数据处理
-
有什么优势?程序可以使用模块化涉及,容易维护、容易扩展、容易升级
-
举例
3.1 输入事件
按键、触摸屏、网络数据,数据完全不一样!
怎么统一?
3.2 输入设备
谁产生输入事件?
输入设备!
里面有哪些函数?初始化、获得数据。
3.3 输入缓冲区
多个输入设备,它们产生的数据放在哪里?用环形缓冲区来保存。
4. 程序设计原则
- 头文件、源文件功能时不一样的
- 头文件:声明,给别人引用的
- 用不到的static函数,就不要放在头文件里声明
- 函数接口容易看懂
- 源文件:定义,实现函数
- 功能独立
- 尽量不使用别的模块
- 头文件:声明,给别人引用的
- 不要太长
- 功能太大的话,拆分为子函数
- 全局变量不要暴露出去
4.1 举例
数据。
[外链图片转存中…(img-UPzVGzQ2-1648726428959)]
3.3 输入缓冲区
多个输入设备,它们产生的数据放在哪里?用环形缓冲区来保存。
4. 程序设计原则
- 头文件、源文件功能时不一样的
- 头文件:声明,给别人引用的
- 用不到的static函数,就不要放在头文件里声明
- 函数接口容易看懂
- 源文件:定义,实现函数
- 功能独立
- 尽量不使用别的模块
- 头文件:声明,给别人引用的
- 不要太长
- 功能太大的话,拆分为子函数
- 全局变量不要暴露出去