单片机程序架构——三层架构——用户“会笑的星星”撰稿

发布时间:2020-03-27 阅读量:2451 来源: 我爱方案网 作者:

【编者按】在上一篇文章“单片机程序——二层架构”中作者谈过设计二层架构的方法,也强调了二层架构的优点在于分离硬件层,使得硬件层更具有通用性。但缺点在于应用层的程序过于耦合,导致应用层的程序比较复杂并且复用性不够。为了解决二层架构的缺点,还需要把应用层一分为二,也就是中间层以及应用层,这两层与硬件层一起构成了所谓三层架构。


这里多出来的中间层就是对应用层功能的抽象。抽象的好处在于一个是简化了应用层的设计,另一个好处就是就是中间层的代码复用性提高。这里我将以之前在“串口模块”这篇文章中谈到的串口功能为例,看看如何设计单片机程序的三层架构。关于如何抽象,建议看看我之前写的“程序的抽象”那篇文章。这里我以之前讲过的串口模块为例,来说说设计三层架构的方法。我先描述一下问题。

 

1)应用层利用串口发出调试信息,这些调试信息包括 : 字符串、十六进制的字符形式。

2)应用层处理串口接收到的信息以便完成一些事情。

 

在解决上述两个问题之前,我再次强调一下程序分层的一个重要原则 --- 尽量只有上层调用下层,而不是相反。如果要从下层调用上层,原则上采用回调函数的方式来实现,但在实际中需要根据情况来决定

 

先看问题(1),解决串口发出调试信息的问题。我们按照三层架构的方法来设计这个功能。

 

首先我们看应用层。

 

按要求,需要向应用层需要提供两个抽象接口。一个用于发送任意字符串。另一个用于发送任意长度的十六进制对应的字符数据。如下的例子所示。

 

//应用层

#include "mid_serial.h"

 

test_dat[6] = {0x01,0x02,0x03,0x04,0x05,0x06}

 

//函数功能: 发送字符串

serial_u0_send_str("tx:");

//函数功能: 将十六进制转换为对应的字符并发送出去

serial_u0_send_hex_char(test_dat,5);

 

结果:

tx:01 02 03 04 05

 

serial_u0_send_str()以及serial_u0_send_hex_char()函数在“串口模块“那篇文章讲过,这里不再详细说明。

 

这两个抽象接口的具体实现单独的封装在一个名为mid_serial.c的文件中,接口声明在mid_serial.h中,这两个文件构成我们所谓的中间层。为了能让中间层发送相关的功能工作,这个mid_serial.c文件内还需要做两件事。一个是定义中间层需要的变量,比如串口发送缓存区等。另一个是管理串口发送缓冲区的函数,这个函数属于中间层自己的,但是需要应用层调用才能让中间层工作。我把这几个函数的声明写在下面,具体源码见“串口模块”那篇文章。

 

//中间层 --- mid_serial.h

#define UART_TX_BUF_LENGTH_32  31

#define UART_TX_BUF_LENGTH_64  63

#define UART_TX_BUF_LENGTH_128 127

 

#define UART0_TX_BUF_COUNT    UART_TX_BUF_LENGTH_64

//设置发送缓冲区长度,这里的长度是64个字节

#define UART0_TX_FIFO_LENGTH  (UART_TX_BUF_LENGTH_64+1)

 

//初始化串口模块相关参数

extern void serial_parameters_init(void );

//串口发送缓冲区的管理函数,用于执行具体的数据发送。这个函数需要在应用层定时调用,

//定时间隔由波特率决定

extern void serial_u0_send_manage();

//发送字符串

extern void serial_u0_send_str(unsigned char *ptxs);

//发送十六进制对应的字符格式数据

extern void serial_u0_send_hex_char(unsigned char *ptxd,unsigned char len);

 

这样,中间层关于发送部分的代码就完成了。你可以发现,这样做一个好处是可以让这个模块更为通用,能方便的应用在其他项目上。另一个好处是让整个程序的结构更为清晰。

 

到目前为止,我们完成了问题(1)的应用层以及中间层的设计。最后我们还需要设计的是中间层需要的硬件层功能。

 

硬件层一个是需要提供串口的初始化函数确保硬件串口能工作。另一个是需要为中间层提供数据写入接口,以便把数据写到硬件寄存器,如下图所示。

 

//硬件层 --- hal.h

extern void hal_uart_init(void );

//这个函数被中间层serial_u0_send_manage()直接调用

extern void hal_uart_set_tx_data(unsigned char tx_data);

 

上述两个函数在讲“单片机程序设计---二层架构”中提到过,这里也不再说明。

 

至此,问题(1)的三层架构就设计完成了。为了更好的理解,我把他们之间的调用关系写在下面。

 

 

 

//应用层

#include "hal.h"

#include "mid_serial.h"

 

unsigned char clk_2ms;

 

void app_clk_2ms(void )

{

  serial_u0_send_manage();  

}

 

main()

{

  hal_uart_init();  

  serial_parameters_init();

  //发送字符串

  serial_u0_send_str("tx:");

  while(1)

  {

     if(clk_2ms)

     {

       clk_2ms = 0;

       app_clk_2ms();

     }  

  }

}

 

//中间层 --- mid_serial.h

//以下函数的实现在mid_serial.c

extern void serial_parameters_init(void );

extern void serial_u0_send_manage(void );

extern void serial_u0_send_str(unsigned char *ptxs);

extern void serial_u0_send_hex_char(unsigned char *ptxd,unsigned char len);

 

//硬件层 --- hal.h

extern void hal_uart_init();

extern void hal_uart_set_tx_data(unsigned char tx_data);

 

你可以看到,从应用层到硬件层都是上层调用下层,没有出现下层调用上层的情况。你可能会问,如果出现了下层调用上层的情况怎么办,这就是问题(2)中要解决的问题。

 

根据问题(2),应用层需要处理串口接收到的数据,数据流要从硬件寄存器 -> 中间层 -> 应用层。这个过程与串口数据发送相反。因此,这里会涉及硬件层调用中间层的函数,中间层调用应用层的函数。虽然前面我说过,下层调用上层,一般采用回调函数的方式实现。但是我在单片机程序设计---二层架构中说过,从程序实现的复杂度以及软件的通用性的角度来说并不建议使用回调函数来实现下层调用上层(如果没有看那篇文章的话建议先看看),而是在下层直接调用上层函数。

 

我们还是从应用层开始来解决问题(2)。

 

应用层需要处理从中间层过来的数据。一般而言,中间层需要把数据缓存区指针以及数据长度给到应用层。这里就会涉及中间层(下层)调用应用层(上层)的问题。我们先编写应用层函数,用于处理来自中间层的数据。如下代码。

 

//应用层代码

//函数功能:应用层处理中间层过来的数据

void app_u0_rx_handle(unsigned char *p, unsigned char len)

{

}

 

接下来我们编写中间层的代码,用于把硬件层接收到的数据给到应用层,这是通过中间层直接调用app_u0_rx_handle()函数实现,代码如下。

 

//中间层代码 --- mid_serial.c

//函数功能:该函数检测接收数据缓冲区中是否有数据。如果有数据,且接收完成,则调用

//app_u0_rx_handle()函数。具体可以看“串口模块”那篇文章的相关部分。

serial_u0_receiver_data_manage(void )

{

   if(ser0.rx.len != 0 && ser0.rx.timeout == 0)

   {

     SERIAL0_RECEIVER_FUNCTION(ser0.rx.fifo, ser0.rx.len);

     ser0.rx.len = 0;

   }

}

 

上述代码中,SERIAL0_RECEIVER_FUNCTION()是在mid_serial.h中定义的宏。如下图所示。之所以这么做是为了把中间层代码要修改的地方集中在mid_serial.h,这样方便后续修改。

 

//中间层 --- mid_serial.h

 

#define  SERIAL0_RECEIVER_FUNCTION(fifo, len)  app_u0_rx_handle(fifo, len)

 

由于硬件层需要把数据给到中间层,因此中间层需要实现获取硬件层过来的数据接口,以便硬件层把数据给到中间层。这个接口如下。

 

 

//中间层 --- mid_serial.c

 

//函数功能:把串口寄存器过来的数据保存到串口接收缓冲区中,这个函数一般在属于硬件层

//中的串口接收中断中直接调用

void serial_u0_receiver_data(unsigned char rx_dt)

{

   ser0.rx.fifo[ser0.rx.len++] = rx_dt;

}

 

这样,中间层用于处理接收的就有两个函数。一个是serial_u0_receiver_data_manage()函数,用于接收到完整的数据后调用应用层函数去处理数据。另一个是serial_u0_receiver_data()函数,用于硬件层调用以把串口寄存器数据给到中间层。

 

下面把中间层的内容汇总一下。

//中间层 --- mid_serial.h

 

#define UART_TX_BUF_LENGTH_32  31

#define UART_TX_BUF_LENGTH_64  63

#define UART_TX_BUF_LENGTH_128 127

 

#define UART0_TX_BUF_COUNT    UART_TX_BUF_LENGTH_64

//设置发送缓冲区长度,这里的长度是64个字节

#define UART0_TX_FIFO_LENGTH  (UART_TX_BUF_LENGTH_64+1)

 

//串口发送相关的声明

extern void serial_parameters_init(void );

 

extern void serial_u0_send_manage(void );

extern void serial_u0_send_str(unsigned char *ptxs);

extern void serial_u0_send_hex_char(unsigned char *ptxd,unsigned char len);

 

//串口接收相关声明

extern void serial_u0_receiver_data_manage();

extern void serial_u0_receiver_data(uint8_t rx_dt);

 

最后,我们需要在硬件层中调用serial_u0_receiver_data()函数,以便把串口寄存器内的数据保存到串口数据接收缓冲区。

 

//硬件层 --- hal.c

 

//串口接收中断

DEFINE_ISR(UART0_RX_ISR,0x10)

{

  if(_t1af)

  {

    _t1af = 0;

   serial_u0_receiver_data(_txr_rxr); //_txr_rxr为单片机的串口接收寄存器

  }

}

 

要注意,我在中断中直接调用了中间层的函数,没有使用回调函数。

 

这样,我们就按照三层架构的思路解决了问题(2)。同样,为了更好的理解问题(2)的三层架构,我把他们之间的调用关系汇总在下面。

 

//应用层

 

#include "hal.h"

#include "mid_serial.h"

 

void app_u0_rx_handle(*p,len)

{

   //应用层处理数据

}

 

void app_clk_2ms(void )

{  

  //查询串口接收缓冲区中是否有数据且接收完成,如果完成调用应用层函数app_u0_rx_handle()具体可看"串口模块"那篇文章。

  serial_u0_receiver_data_manage();

}

 

main()

{

  hal_uart_init();    

  serial_parameters_init();

  while(1)

  {

     if(clk_2ms)

     {

       clk_2ms = 0;

       app_clk_2ms();

     }   

  }

}

 

//中间层 --- mid_serial.h

 

//mid_serial.h文件中添加串口接收相关声明,他的实现在mid_serial.c

extern void serial_u0_receiver_data_manage();

extern void serial_u0_receiver_data(uint8_t rx_dt);

 

//硬件层 --- hal.c

 

DEFINE_ISR(UART0_RX_ISR,0x10)

{

  if(_t1af)

  {

    _t1af = 0;

   serial_u0_receiver_data(_txr_rxr); //_txr_rxr为单片机的串口接收寄存器

  }

}

 

最开始的两个问题通过三层架构解决了,最后看看程序文件如何组织。

ke1.png 

 

工程内的文件,中间层不同功能的模块都保存在mid文件下。

 

ke2.png 

工程外的文件

 

ke3.png 

工程外的文件,mid文件夹中的内容

 

 

工程外apphal等文件在单片机程序架构 --- 二层架构中讲过,这里就不再说了。

 

一般而言,把单片机程序分为三层很多时候是够用的。三层中的硬件层是对硬件功能的抽象,中间层时对应用层的抽象,最后应用层调用中间层完成自己的功能。这样,中间层、硬件层就具有比较好通用性,同时程序的架构也比较清晰,减少bug的出现。

 

这篇文章有点长,也不太好懂。如果想更好的理解这些东西,还需要看完之前写过的几篇文章希望对你有用。

 

 

本文作者:会笑的星星是一名设计开发工程师,在平台上的用户名为“ 1585292050XQYe,有多年的物联网安防产品设计开发经验。

 


 

 

 

 


相关资讯
无源晶振YSX321SL应用于高精度HUD平视显示系统YXC3225

在现代汽车行业中,HUD平视显示系统正日益成为驾驶员的得力助手,为驾驶员提供实时导航、车辆信息和警示等功能,使驾驶更加安全和便捷。在HUD平视显示系统中,高精度的晶振是确保系统稳定运行的关键要素。YSX321SL是一款优质的3225无源晶振,拥有多项卓越特性,使其成为HUD平视显示系统的首选。

拥有卓越性能的高精度超薄低功耗心电贴—YSX211SL

随着医疗技术的进步,心电监护设备在日常生活和医疗领域中起到了至关重要的作用。而无源晶振 YSX211SL 作为一种先进的心电贴产品,以其独特的优势在市场上备受瞩目。

可编程晶振选型应该注意事项

对于可编程晶振选型的话,需要根据企业的需求选择。在选择可编程晶振的时候注重晶振外观、晶振的频率、晶振的输出模式、晶振的型号等等,这些都是要注意的,尤其是晶振的频率和晶振输出模式以及晶振的型号都是需要注意的。

性能高的服务器—宽电压有源晶振YSO110TR 25MHZ,多种精度选择支持±10PPM—±30PPM

在现代科技发展中,服务器扮演着越来越重要的角色,为各种应用提供强大的计算和数据存储能力。而高品质的服务器组件是确保服务器稳定运行的关键。YSO110TR宽电压有源晶振,作为服务器的重要组成部分,具备多项优势,成为业界必备的可靠之选。

差分晶振怎么测量

其实对于差分晶振怎么测量方式有很多种,主要还是要看自己选择什么样的方式了,因为选择不同的测量方式步骤和操作方式是不同的。关于差分晶振怎么测量的方式,小扬给大家详细的分享一些吧!