一种嵌入式程序的模块设计规范



嵌入式程序中,有时一个功能模块的使用会跨越多个物理器件。比如DDS芯片9837至少会用到SPI和GPIO。对于这种情形,最简单的做法是直接将所用到的物理寄存器包含在这个模块内部,写死。这样做没有什么问题,但是这样写,系统本身的逻辑代码和物理寄存器读写代码纠缠在一起,可读性和可移植性都不好。

好的做法是这样的,首先要提炼出接口。对于9837而言,它的底层物理器件接口,就是spi和gpio,接口首先要确定下来。对于spi的接口也许可以写作:

spi_out(int channel, unsigned char *buf, int len);

对于gpio的接口,可以是

gpio_set(int channel);

gpio_clear(int channel);

gpio_toggle(int channel);

这是最基本的接口。但是接下来呢?是否需要内部通过switch()来将具体操作导向各个寄存器读写?这种做法也是最容易想到的办法,但是违背了模块封装原则。它把逻辑上没有关联的一些寄存器读写代码汇聚到了一个函数里,当然,你也可以解释他们都从属于一类物理器件,聚集起来也没有什么大不了的。但是它至少破坏了两个程序开发的原则:第一,可移植性变差,如果其他项目需要9837这个模块,代码无法快速地拆分出去。因为SPI和GPIO的代码和别的模块中所使用的代码是混在一起的。另外,可维护性也差,因为如果增添新的器件,你会修改一个既有的函数,这很可能会影响既有功能。

那么正确的做法是什么呢?我们的工具是函数指针,用它来实现依赖反转。接口是不应该依赖于具体实现的(接口无论.h.c文件中都不应该出现与具体实现相关的代码)。具体实现应该依赖接口。具体的做法类似如下的操作,我们以GPIO为例说明:gpio接口除了上述三个接口函数外,额外增添一个初始化函数:

int gpio_init(FN_STD init, FN_STD set, FN_STD clear, FN_STD toggle);

这个函数的返回值,返回的是在三个接口函数中channel句柄,FN_STD的定义为:

typedef void (*FN_STD)();

经过这样改造后的9837代码,逻辑代码与寄存器操作分离了。逻辑代码可以放在9837.c中,移植时无需更改这个文件,但它已经包含了完整的9837芯片的访问逻辑。寄存器操作部分相对独立,可以整合在一个名为9837_low_level.c的文件中,在移植的时候,逐个文件,修改其中的寄存器访问代码即可。寄存器访问代码因为已经依据不同的功能拆分为一个个具有明确语义的小函数,所以移植工作也可以很快完成。

我们下面以代码实例展示一下:

#ifndef GPIO_H
#define GPIO_H

#define MAX_CHANNEL		4

//回调函数
typedef void (*FUN_STD_GPIO)(void);
typedef int (*FUN_CHK_GPIO)(void);
typedef int GPIO_CH;

//GPIO子系统初始化
void GPIO_Init(void);
//GPIO通道初始化
int GPIO_Register(FUN_STD_GPIO fn_init, FUN_STD_GPIO fn_on, FUN_STD_GPIO fn_off, FUN_CHK_GPIO fn_is_on);
//GPIO开启
void GPIO_On(GPIO_CH ch);
//GPIO关闭
void GPIO_Off(GPIO_CH ch);
//GPIO切换
void GPIO_Toggle(GPIO_CH ch);

上面的代码是gpio.h定义的GPIO器件的注册和访问接口。下面是源代码部分:

#include "gpio.h"

typedef struct _GPIO_CH_DESC
{
	FUN_STD_GPIO init;
	FUN_STD_GPIO on;
	FUN_STD_GPIO off;
	FUN_CHK_GPIO is_on;
}GPIO_CH_DESC, *PGPIO_CH_DESC;

GPIO_CH_DESC gGpioCh[MAX_CHANNEL];
int gGpioNextCh = 0;

//GPIO子系统初始化
void GPIO_Init(void)
{
}

//GPIO通道初始化
int GPIO_Register(FUN_STD_GPIO fn_init, FUN_STD_GPIO fn_on, FUN_STD_GPIO fn_off, FUN_CHK_GPIO fn_is_on)
{
	if(gGpioNextCh >= MAX_CHANNEL) return -1;

	gGpioCh[gGpioNextCh].init = fn_init;
	gGpioCh[gGpioNextCh].on = fn_on;
	gGpioCh[gGpioNextCh].off = fn_off;
	gGpioCh[gGpioNextCh].is_on = fn_is_on;

	fn_init();

	return gGpioNextCh++;
}

//GPIO开启
void GPIO_On(GPIO_CH ch)
{
	if(ch>=0 && ch<gGpioNextCh) gGpioCh[ch].on();
}
//GPIO关闭
void GPIO_Off(GPIO_CH ch)
{
	if(ch>=0 && ch<gGpioNextCh) gGpioCh[ch].off();
}
//GPIO切换
void GPIO_Toggle(GPIO_CH ch)
{
	if(ch>=0 && ch<gGpioNextCh)
	{
		if(gGpioCh[ch].is_on()) gGpioCh[ch].off();
		else gGpioCh[ch].on();
	}
}

可以看到,上面的代码中未出现任何与寄存器读写相关的内容。因为寄存器读写涉及到具体寄存器操作,与GPIO这个接口层定义是无关的。

下面是一个具体的GPIO接口定义,首先是头文件:

#ifndef GPIO_PIN80_H
#define GPIO_PIN80_H

//特定通道
extern int GPIO_PIN80;
//初始化GPIO_PIN80模块;
void GPIO_PIN80_Init(void);

#endif

头文件中仅包含一个通道句柄,定义了一个特定管脚PIN80对应的GPIO通道号;以及一个初始化函数,初始化函数中不涉及任何与GPIO接口初始化相关的知识,这些东西仅对制作这个小模块的模块作者是可见的,对用户是隐藏的。最后是这个小模块的实现函数:

#include "LPC17xx.h"
#include "gpio.h"
#include "gpio_pin80.h"

int GPIO_PIN80 = -1;

void GPIO_PIN80_Init_Inner(void);
void GPIO_PIN80_On(void);
void GPIO_PIN80_Off(void);
int GPIO_PIN80_IsOn(void);

void GPIO_PIN80_Init(void)
{
	GPIO_PIN80 = GPIO_Register(GPIO_PIN80_Init_Inner,
		GPIO_PIN80_On,
		GPIO_PIN80_Off,
		GPIO_PIN80_IsOn);
}

void GPIO_PIN80_Init_Inner(void)
{
	LPC_PINCON->PINSEL0 &= (~((0x11)<<10)); //pin80. p0.5 gpio
	LPC_GPIO0->FIODIR |= (1<<5);
	GPIO_PIN80_On();
}

void GPIO_PIN80_On(void)
{
	LPC_GPIO0->FIOCLR |= (1<<5);
}

void GPIO_PIN80_Off(void)
{
	LPC_GPIO0->FIOSET |= (1<<5);
}

int GPIO_PIN80_IsOn(void)
{
   return (LPC_GPIO0->FIOPIN & (1<<5)) ? 0 : 1;
}

实际使用该模块的代码为:

        //...
	GPIO_PIN80_Init();
        //...
	while(1)
	{
		GPIO_Toggle(GPIO_PIN80);
                //...
         }

It‘s beautiful, right?

时间: 2024-10-08 11:13:47

一种嵌入式程序的模块设计规范的相关文章

全面解析《嵌入式程序员应该知道的16个问题》

文章为转载文章,写的很好,和大家分享下,原文连接如下: ----Sailor_forever分析整理,[email protected] http://blog.csdn.net/sailor_8318/archive/2008/03/25/2215041.aspx 1.预处理器(Preprocessor) 2.如何定义宏 3.预处理器标识#error的目的是什么? 4.死循环(Infinite loops) 5.数据声明(Data declarations) 6.关键字static的作用是什么

【转】嵌入式程序员应该知道的16个问题

全面解析<嵌入式程序员应该知道的16个问题> ----Sailor_forever分析整理,[email protected] http://blog.csdn.net/sailor_8318/archive/2008/03/25/2215041.aspx 1.预处理器(Preprocessor) 2.如何定义宏 3.预处理器标识#error的目的是什么? 4.死循环(Infinite loops) 5.数据声明(Data declarations) 6.关键字static的作用是什么? 7.

嵌入式程序员应知道的0x10个C语言Tips

[1].[代码] [C/C++]代码 跳至 [1] ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

嵌入式程序员应知道的0x10个基本问题

来源:网络 1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL我在这想看到几件事情:1) #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)2)懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的.3) 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉

写给嵌入式程序员的循环冗余校验(CRC)算法入门引导

写给嵌入式程序员的循环冗余校验(CRC)算法入门引导 http://blog.csdn.net/liyuanbhu/article/details/7882789 前言 CRC校验(循环冗余校验)是数据通讯中最常采用的校验方式.在嵌入式软件开发中,经常要用到CRC 算法对各种数据进行校验.因此,掌握基本的CRC算法应是嵌入式程序员的基本技能.可是,我认识的嵌入式程序员中能真正掌握CRC算法的人却很少,平常在项目中见到的CRC的代码多数都是那种效率非常低下的实现方式. 其实,在网上有一篇介绍CRC

C#1(.net和C#的关系、VS与.net的对应关系、VS2012常用的几种应用程序、C#定义一个类的方法、类页面内容的解释、定义Person的类、调用Person类的方法、命名规范、数值类型)

1..net和C#的关系 .net是一个开发平台,C#是应用在.net平台上的一种语言.   2.VS与.net的对应关系  3.VS2012常用的几种应用程序 第一种是Windows窗体应用程序,也即是我们常用的C/S端的应用软件: 第二种是控制台应用程序,主要是用来学习调试C#代码的(老师上课应用的模式): 第三种是空Web应用程序,建立空的网页模式,B/S模式: 第四种是Web 窗体应用程序,建立后会生成一些常用的网页组件和功能,例如JS.image等,也是B/S模式. 4.C#定义一个类

嵌入式程序跑飞源头定位方法

在调试嵌入式程序时经常会遇到程序"莫名其妙"的跑飞,而这类问题一般仿真是不容易找到问题源的.今天灵光一闪,我想到了一个方法可以帮助我们定位问题源,而在实际的使用后,发现这个方法的确可行,也帮助我解决了问题. 先总结一下造成嵌入式程序跑飞的原因: 1. 内存操作错误,如alloc/memset/memcpy等使用错误: 2. 指针使用错误,如使用了空指针: 3. 数组操作错误,如数组越界: 现在开始讲解定位该类问题的方法,以裸机程序为例,带有操作系统的程序方法类似 裸机程序大体的结构如下

几种C#程序读取MAC地址的方法

以下是收集的几种C#程序读取MAC地址的方法,示例中是读取所有网卡的MAC地址,如果仅需要读取其中一个,稍作修改即可. 1 通过IPConfig命令读取MAC地址 ///<summary>/// 根据截取ipconfig /all命令的输出流获取网卡Mac///</summary>///<returns></returns>publicstatic List<string> GetMacByIPConfig(){  List<string&

微信小程序蓝牙模块

蓝牙部分知识 关于Service: 每个设备包含有多个Service,每个Service对应一个uuid 关于Characteristic 每个Service包含多个Characteristic,每个Characteristic对应一个uuid 如何得到数据 我们想要的数据是包含在每一个Characteristic 微信小程序目前提供的蓝牙API:详细参数请见小程序开发文档 1.操作蓝牙适配器的4个API   wx.openBluetoothAdapter //初始化蓝牙适配器 wx.close