网站后台发邮件,wordpress插件中文网,网站大数据怎么做,重庆网站建设选卓光本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写#xff0c;需要的同学可以在这里获取#xff1a; https://item.taobao.com/item.htm?id728461040949
配套资料获取#xff1a;https://renesas-docs.100ask.net
瑞萨MCU零基础入门系列教程汇总#xff1a; ht…本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写需要的同学可以在这里获取 https://item.taobao.com/item.htm?id728461040949
配套资料获取https://renesas-docs.100ask.net
瑞萨MCU零基础入门系列教程汇总 https://blog.csdn.net/qq_35181236/article/details/132779862 第2章 面向过程与面向对象
本章目标
理解面向过程与面向对象编程方法的特点锻炼面向对象编程的能力
2.1 特性对比
2.1.1 概念介绍
学习单片机开发时我们写的第 1 个程序通常是点灯示例代码如下
// 点灯
void main()
{while (1){led_on();delay_ms(500);led_off();delay_ms(500);}
}在 main 函数中是一个 while 循环首先调用 led_on()函数点亮 LED然后延迟一阵子接着调用 led_off()熄灭 LED最后延迟一阵子。
这个程序就是面向过程的它的编程方法是先分析出解决问题所需的步骤然后针对每一步骤编写函数最后依次调用这些函数。
对于面向对象最初级的理解就是使用结构体或类来封装硬件的操作C 语言中没有类就使用结构体示例代码如下
// 点灯
struct led
{void (*on)(void);void (*off)(void);
};static ra_led_on(void)
{// 点亮 LED
}static ra_led_off(void)
{// 熄灭 LED
}static struct led g_led {.on ra_led_on,.off ra_led_off,
};void main()
{while (1){g_led.on();delay_ms(500);g_led.off();delay_ms(500);}
}第 2~5 行:对于 LED它是一个“对象”抽象出一个结构体“struct led”里面有 2 个函数指针。第 18~19 行定义了一个结构体变量 g_led填充了结构体里的 2 个函数指针让它们指向具体的函数。第 23 行开始的 main 函数跟面向过程的 main 函数类似只不过操作 LED 时是调用结构体 g_led 的函数指针。
上述程序就是面向对象的它的编程方法是把问题中的事务分解成各个对象在对象里描述事务的属性、行为最后使用对象来解决问题。
上述面向对象的程序过于粗浅其实对于 main 函数里实现的功能也可以抽象为一个结构体。比如可以抽象出一个结构体“struct business”表示“业务”。示例代码如下
struct business{void (*run)(void);
};void led_blink(void)
{while (1){g_led.on();delay_ms(500);g_led.off();delay_ms(500);}
}struct business g_business {.run led_blink,
};void main()
{g_business.run();
}第 1~3 行把业务抽象为结构体“struct business”里面有一个函数指针 run。第 5~14 行实现了 LED 的闪烁功能。第 16~18 行定义了一个“struct business”结构体 g_business。第 20~23 行在 main 函数里调用 g_business 结构的 run 函数指针。
我们把程序分解成了“业务”、“LED”在实现“业务”时需要操作硬件“LED”。
2.1.2 优缺点对比
使用面向过程编写程序时符合人类的认知过程比较自然。它的效率也比较高因为省去了结构体的初始化调用函数时也比较直接无需通过结构体进行调用。
使用面向过程编写程序时更容易维护、复用、扩展。
怎么理解面向对象的优点呢
假设有一个产品更新了 2 代第 1 代使用 I2C 接口的屏幕第 2 代使用 SPI 接口的屏幕。怎么使用一套代码支持这两代产品有三种方法宏开关、使用条件判断、使用结构体。
我们要实现在屏幕上显示字符串的函数 lcd_print使用宏开关时示例代码如下
// LCD
#ifdef LCD_VER1
void i2c_lcd_print(char *str)
{// 在 I2C 屏幕上显示字符串
}
#else
void spi_lcd_print(char *str)
{// 在 SPI 屏幕上显示字符串
}
#endifvoid lcd_print(char *str)
{
#ifdef LCD_VER1i2c_lcd_print(str);
#elsespi_lcd_print(str);
#endif
}对于第 14 行的 lcd_print 函数当定义宏 LCD_VER1 时它调用 i2c_lcd_print 操作第1 代的屏幕当没有定义宏 LCD_VER1 时它调用 spi_lcd_print 操作第 2 代的屏幕。这种方法的优点是编译出来的可执行程序比较小因为它只支持 1 种屏幕要么支持第 1 代要么支持第 2 代。缺点是同一个可执行程序不能既支持第 1 代屏幕也支持第 2 代屏幕。
为了能同时支持两代屏幕假设可以读到屏幕的型号那么可以使用条件判断的方法示例代码如下
// LCD
void i2c_lcd_print(char *str)
{// 在 I2C 屏幕上显示字符串
}void spi_lcd_print(char *str)
{// 在 SPI 屏幕上显示字符串
}void lcd_print(char *str)
{if (read_lcd_type() VER1)i2c_lcd_print(str);elsespi_lcd_print(str);
}第 14 行的代码里先读取屏幕的型号后面根据型号决定调用 i2c_lcd_print 还是spi_lcd_print。
使用条件判断的方法可以同时支持两代屏幕但是有一个缺点效率低。如果需要实现多个屏幕的操作函数比如 lcd_on、lcd_off、lcd_rotate 等等函数这些函数内部都要进行型号的判断。如果要支持更多种屏幕那么就需要更多的条件判断比如
void lcd_print(char *str)
{int type read_lcd_type();if (type VER1)i2c_lcd_print(str);else if (type VER2)spi_lcd_print(str);else if (type VER3)usb_lcd_print(str);......
}这样写出来的代码既低效又丑陋。最好的办法就是使用结构体初始化时指定具体函数以后直接调用示例代码如下
// 使用结构体操作 LCD
struct lcd{void (*on)(void);void (*off)(void);void (*print)(const char *str);
};// 根据 LCD 的型号设置 struct lcd 结构体的函数指针
void lcd_init(struct lcd *plcd)
{int type read_lcd_type();if (type VER1){plcd-on i2c_lcd_on;plcd-off i2c_lcd_off;plcd-print i2c_lcd_print;}else if (type VER2){plcd-on spi_lcd_on;plcd-off spi_lcd_off;plcd-print spi_lcd_print;}else if (type VER3){plcd-on usb_lcd_on;plcd-off usb_lcd_off;plcd-print usb_lcd_print;}......
}// 使用
struct lcd g_lcd;
void main(void)
{lcd_init(g_lcd); // 初始化g_lcd.on(); // 启动 LCDg_lcd.print(www.100ask.net); // 显示文字
}第 2~6 行把屏幕抽象为一个结构体“struct lcd”里面有 on、off、print 等函数指针。第 9~31 行根据屏幕的型号设置结构体让它的函数指针指向具体屏幕的操作函数。第 35 行的 main 函数在初始化 g_lcd 后就可以直接使用了效率更高。
使用面向对象的思想把硬件的操作封装在一个结构体里甚至把一个业务的操作封装在一个结构体里。初次接触这种风格的代码时不容易理解。特别是在比较复杂的系统里这些封装的层次更多层层封装和多次跳转之下程序更难理解。但是一旦理解之后就会发现程序设计的精妙。一旦习惯了面向对象的编程方法你就不会再使用面向过程的编程方法。
2.2 面向对象的程序设计方法
在《代码大全》第 5 章中把程序设计分为这几个层次
第 1 层软件系统就是整个系统、整个程序第 2 层分解为子系统比如可以把整个程序拆分为输入系统、显示系统、业务系统第 3 层分解为类在 C 语言里没有类可以分解为结构体第 4 层分解为子程序就是实现那些结构体(实现里面的函数指针)
2.2.1 分解为子系统
一个系统必定可以分为这 3 大子系统输入系统、处理系统、输出系统。 这种拆分方法过于粗糙在复杂场景里需要进一步拆分。比如对于输入子系统如果产品有用户输入、传感器、远程控制等功能那么输入子系统可以拆分为用户输入子系统、传感器子系统、远程控制子系统如下 比如对于输出子系统可能会涉及显示、操作各类设备、数据保存那么就可以细分为显示子系统、设备操作、数据子系统如下 对于中间的处理子系统它根据输入提供输出。不同的产品有不同的处理逻辑对于智能家居产品它根据用户按键、或者根据预设时间、或者根据远程控制命令去操作各类家居产品对于工业机器人它根据各类信号执行特定的动作。处理子系统是一个集成者根据业务逻辑采集左侧各类输入子系统的数据操作右侧各类输出子系统。以智能家居产品为例处理子系统又可以细分为用户关系处理子系统、设备控制子系统、配置子系统等
等如下 2.2.2 分解为结构体
怎么实现某个子系统关键在于抽象出合适的结构体。比如对于用户输入子系统可以对外提供两个接口函数初始化、获得按键数据。关键就在于怎么表示“按键数据”。对于用户输入来源有多种按键、鼠标、触摸屏。使用怎样的结构体能统一地描述这些不同来源的输入事件可以参考 Linux 中的代码在 Linux 中有如下结构体
struct input_event{struct timeval time;__u16 type;__u16 code;__s32 value;
};第 2 行的 time表示事件发生的时间。第 3 行 type 表示事件类型有下面这些取值EV_KEY 表示按键EV_REL 表示相对位移比如鼠标EV_ABS 表示绝对位移比如触摸屏。
#define EV_KEY 0x01
#define EV_REL 0x02
#define EV_ABS 0x03第 4 行 code 表示编码比如按键有 A、B、C 等按键的编码值鼠标有 X、Y 等方向的编码值触摸屏有 X、Y、Z 等方向的编码值。第 5 行 value 表示取值比如对于按键0 表示松开1 表示按下对于鼠标它就表示偏移值对于触摸屏它就表示触点坐标。
举几个例子比如按下键盘上的 A 键时怎么使用 input_event 结构体表示它如下
{time, EV_KEY, KEY_A, 1}往右边移动鼠标 100 个单位往下移动鼠标 200 个单位怎么使用 input_event 结构体表示它需要用 2 个 input_event 结构体如下
{time, EV_REL, REL_X, 100}{time, EV_REL, REL_Y, 200}在触摸屏(100, 200)的位置点击、松开怎么使用 input_event 结构体表示它需要用3 个 input_event 结构体如下
{time, EV_ABS, ABS_X, 100}{time, EV_ABS, ABS_Y, 200}{time, EV_ABS, ABS_Z, 1} // 表示按下考虑完善的结构体有助于涉及实现子系统有了 input_event 结构体后用户输入子系统对外提供的接口就可以如下定义
int Init_User_Input_System(void);int Get_Input_Event(struct input_event *ptEvent);假设需要同时支持多种用户输入设备那么还可以继续抽象出其他结构体比如用来描述输入设备的“struct input_dev”如下
struct input_dev {const char *name;int (*Init)(void);};对于每一个输入设备都会为它构建一个“struct input_dev”结构体。
2.2.3 分解为子程序
还是以用户输入系统为例假设需要支持按键、鼠标、触摸屏那么可以构造对应的 3个“struct input_dev”结构体给里面的 Init 函数指针提供按键、鼠标、触摸屏的初始化函数最重要的是分别注册中断处理函数。
在按键中断处理函数里消除抖动、构造“struct input_event”结构体并把它写入一个环形缓冲区。在鼠标中断处理函数里解读鼠标数据、构造“struct input_event”结构体并把它写入一个环形缓冲区。在触摸屏中断处理函数里通过 I2C 等接口读取触点数据、构造“struct input_event”结构体并把它写入一个环形缓冲区。
程序框图如下 如果使用多任务系统那么可以把上图的环形缓冲区替换为消息队列各类中断程序写消息队列时可以唤醒等待输入事件的任务。
2.2.4 结构体的继承
在单片机软件开发中基本使用的是 C 语言很少使用 C。C 语言是面向过程的语言但是我们可以使用结构体实现面向对象的开发。但是结构体跟类有很大的差别。
在 C中实现类时可以有继承关系。在 C 语言中使用结构体时也可以使用继承在RT-Thread 中大量使用到结构体的继承。比如人类、狗都属于生物生物都有性别、体重等属性那么可以如下定义生物、人类、狗三个结构体人类和狗都继承了生物的特性
struct organism
{int sex; // 性别int weight; // 体重
};struct person
{struct organism parent; // 父类const char *school; // 毕业学校
};struct dog
{struct organism parent; // 父类const char *species; // 种类
};
使用结构体的继承时“父结构体”必须是“子结构体”的第一个成员。这样有一些好处比如要实现一个打印体重的函数可以只实现一个版本示例代码如下
void PrintWeight(struct organism *p)
{printf(weight : %d\n, p-weight);
}void main(void)
{struct person per {{1, 100}, USTC};struct person dog {{1, 10}, Husky};PrintWeight(per);PrintWeight(dog);
}第 1 行实现了一个“打印生物体重”的函数。第 8、9 行分别定义了一个“人”、“狗”的结构体。第 11、12 行可以使用同一个函数“PrintWeight”来打印人的体重、打印狗的体重。如果不使用结构体的继承那么就需要分别实现人、狗的打印函数示例代码如下
struct person{int sex; // 性别int weight; // 体重const char *school; // 毕业学校
};struct dog{int sex; // 性别int weight; // 体重const char *species; // 种类
};void PrintPersonWeight(struct person *p)
{printf(weight : %d\n, p-weight);
}void PrintDogWeight(struct person *p)
{printf(weight : %d\n, p-weight);
}
void main(void)
{struct person per {{1, 100}, USTC};struct person dog {{1, 10}, Husky};PrintPersonWeight(per);PrintDogWeight(dog);
}2.2.5 结构体的函数指针
结构体的成员里可以使用变量来描述属性使用函数指针来描述方法比如 LED 可以抽象出如下结构体
struct led {void (*on)(void);void (*off)(void);
};对于使用芯片引脚控制的 LED即使引脚不同操作函数也是类似的。那怎么分辨使用哪个引脚呢需要在“struct led”里添加属性以描述引脚如下
struct led {int pin; // 引脚void (*on)(void);void (*off)(void);
};但是在 on、off 指针对应的函数里怎么引用到属性 pinC 语言的结构体里不能想C的类那样使用 this 指针来引用自己所以结构体还需要改进在 on、off 函数指针里增加一个参数增加结构体本身的指针如下
struct led{int pin; // 引脚void (*on)(struct led *p);void (*off)(struct led *p);
};使用“struct led”的示例代码如下
// 点灯
struct led{int pin; // 引脚void (*on)(struct led *p);void (*off)(struct led *p);
};static ra_led_on(struct led *p)
{// 点亮 LED// 操作引脚 p-pin
}static ra_led_off(struct led *p)
{// 熄灭 LED// 操作引脚 p-pin
}static struct led g_led {.pin 100,.on ra_led_on,.off ra_led_off,
};void main()
{while (1){g_led.on(g_led);delay_ms(500);g_led.off(g_led);delay_ms(500);}
}第 8、11 行实现的函数需要从参数“struct led *p”获得要操作哪个引脚p-pin。第 21~25 行实现的结构体需要指定属性 pin使用哪个引脚。第 31、33 行调用 g_led.on、g_led.off 函数指针时需要传输 g_led 的地址。
2.2.6 程序设计原则
怎样把整个系统拆分多个子系统在实现子系统时怎样抽象出各个结构体原则是高内聚低耦合。
高内聚一个子系统或者一个结构体尽可能只完成一个功能即最大限度地耦合。低耦合一个子系统或者一个结构体的实现尽可能少地调用到另一个子系统或另一个结构体的功能。
简单地说就是尽可能让一个子系统或一个结构体的功能比较单一减少对其他子系统或其他结构体的依赖。增强内聚度、降低耦合度的方法
基于接口编程隐藏内部实现的细节模块只对外暴露最小限度的接口形成最低的依赖关系只要对外接口不变模块内部的修改不得影响其他模块删除一个模块应当只影响有依赖关系的其他模块而不应该影响其他无关部分模块的功能尽可能的单一接口函数在头文件中声明内部函数不要放在头文件里声明接口函数在 C 文件中实现内部函数定义为 static 函数内部变量定义为 static尽量少用全局变量要使用全局变量的话不要直接访问使用函数来访问调用者只需要包含头文件函数代码不要太长功能太复杂的话拆分为多个子函数 本章完