打印

串口模块---讲三层架构前的铺垫

[复制链接]
3461|10
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
会笑的星星|  楼主 | 2019-12-29 14:15 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 会笑的星星 于 2019-12-29 14:17 编辑

在说三层架构之前,先介绍一下串口模块的相关函数,这个模块把串口发送以及接收相关的功能给抽象出来了。我后面将以这个模块为例介绍设计三层架构的方法。之所以要以这个模块为例子,是因为如果介绍3层架构的例子过于简单或者过于复杂都不够实用,而串口模块部分没那么简单也没那么难,比较适合做为讲3层架构的例子。另外学习这个模块还有另一个好处,那就是可以应用在你的实际项目中,比如打印调试信息或者用于普通的串口信息收发等等。因此,搞清楚这个模块还是必要且有用的。

串口模块主要分为两个部分,一个部分函数是用来发送信息。一部分函数用来接收串口信息。因为串口的接收部分相对简单,我先从接收部分开始讲。

串口模块主要放在两个文件中,一个是mid_serial.h,一个是mid_serial.c。

首先看一下mid_serial.h文件,这个文件定义了mid_serial.c中用到的函数以及一些宏定义。各个宏定义的意思会在mid_serial.c中做详细解释。
#ifndef _MID_UART_
#define _MID_UART_

#include "hal.h"  //硬件层接口

#define UART_TX_BUF_LENGTH_1    0
#define UART_TX_BUF_LENGTH_2    1
#define UART_TX_BUF_LENGTH_4    3
#define UART_TX_BUF_LENGTH_8   7
#define UART_TX_BUF_LENGTH_16  15
#define UART_TX_BUF_LENGTH_32  31
#define UART_TX_BUF_LENGTH_64  63
#define UART_TX_BUF_LENGTH_128 127
#define UART_TX_BUF_LENGTH_256 255

//在源码中解释
#define UART0_RX_TIMEOUT_TIME  3   
//接收缓冲区长度
#define UART0_RX_FIFO_LENGTH   64

//在源码中解释
#define UART0_TX_BUF_COUNT    UART_TX_BUF_LENGTH_64
//设置发送缓冲区长度
#define UART0_TX_FIFO_LENGTH  (UART_TX_BUF_LENGTH_64+1)

//app_u0_rx_handle() --- 这个函数定义在应用层,用户用来处理串口接收到的数据
//之所以放到这个文件,是因为要移植这个代码的话把要改的东西统一放到一个文件,
//这样便于维护.
#define  SERIAL0_RECEIVER_FUNCTION(fifo, len)  app_u0_rx_handle(fifo, len)

//初始化串口模块相关参数
extern void serial_parameters_init(void );

//串口的数据接收管理
extern void serial_u0_receiver_data_manage();
//将串口数据保存在缓存中
extern void serial_u0_receiver_data(uint8_t rx_dt);

//发送十六进制数据
extern void serial_u0_send_hex(unsigned char *ptxd, unsigned char len);
//发送字符串
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_send_manage(void );

#endif
#include "mid_serial.h"

typedef struct
{
  unsigned char  len;     //串口接收到的数据总长度
  unsigned char  timeout; //串口接收到的连续两个字节之间最大时间间隔
  unsigned char  *fifo;   //指向串口接收缓冲区
}rx_t;

typedef struct
{
  unsigned int   len;    //串口要发送的数据总长度
  unsigned char  pos;    //指向串口发送缓冲区中最后一个要发送的数据
  unsigned char  count;  //指向串口发送缓冲区中当前要发送的数据
  unsigned char  *fifo;  //指向串口发送缓冲区
}tx_t;

struct
{
  rx_t rx;
  tx_t tx;
}ser0;

const unsigned char char_tab[] = "0123456789ABCDEF  ";

unsigned char u0_rx[UART0_RX_FIFO_LENGTH+1];  //串口接收缓冲区,多余了一个位置
unsigned char u0_tx[UART0_TX_FIFO_LENGTH+1];  //串口发送缓冲区,多余了一个位置

//函数功能:把串口寄存器过来的数据保存到串口接收缓冲区中,一般而言,这个函数在单片机的串口的接收中断中调用。
//后面我会演示一个调用的例子.
void serial_u0_receiver_data(uint8_t rx_dt)
{
   if(ser0.rx.len >= UART0_RX_FIFO_LENGTH)
   {
      ser0.rx.len = UART0_RX_FIFO_LENGTH-1;
   }
   ser0.rx.fifo[ser0.rx.len++] = rx_dt;
   //之所以加入一个字符串结尾符,是为了方便利用库函数strstr('string')做字符串匹配,对于接收十六进制数据没有影响.
   ser0.rx.fifo[ser0.rx.len] = '\0'
   //连续接收到的两个字节最大的间隔时间,当ser0.rx.timeout == 0,则/serial_u0_receiver_data_manage()函数
   //会去处理接收到的数据,这个函数在后面会讲。
   ser0.rx.timeout = UART0_RX_TIMEOUT_TIME;
}

//函数功能:ser0.rx.timeout == 0后, 该函数会调用用户函数以处理接收到的数据.
//这个函数需要不停的轮训,因此需要放在一个软件定时器中以查询是否有数据
//要处理.至于软件定时器的时间间隔则根据处理数据的实时性来定,一般放在10ms
//软件定时器是一个不错的选择,这样的话当UART0_RX_TIMEOUT_TIME == 3时,
//接收到的两个字节最大时间间隔就是30ms,一旦ser0.rx.timeout == 0,就调用
//应用层函数以处理数据.
void serial_u0_receiver_data_manage()
{
  if(ser0.rx.len != 0 && ser0.rx.timeout == 0)
  {
   //这个函数就是用户函数,他会把串口缓冲区的数据指针以及数据长度送给它,
   //以便用户处理.这个函数是一个宏,定义在mid_serial.h中,由用户修改为
   //对应的应用层函数.
    SERIAL0_RECEIVER_FUNCTION(ser0.rx.fifo, ser0.rx.len);
    ser0.rx.len = 0;
  }
  //为了方便,这个定时变量放在了这里而没有独立成一个函数.
  if(ser0.rx.timeout)
  {
    ser0.rx.timeout--;
  }
}
下面举一个例子来说明如何使用上述的串口接收代码。
#inlcude "mid_serial.h"

//单片机的串口接收中断
DEFINE_ISR(UART0_RX_ISR,0x10)
{
  if(_t1af)
  {
           _t1af = 0;
          serial_u0_receiver_data(_txr_rxr); //_txr_rxr为单片机的串口接收寄存器
  }
}

//软件定时器10ms
void app_sys_clk_10ms(void )
{
   serial_u0_receiver_data_manage()
}

//用户处理串口接收到的数据,这个函数在 serial_u0_receiver_data_manage()中调用
void app_u0_rx_handle(unsigned char *p, unsigned char len)
{
   
}
串口接收部分就说完了,下面说一下串口数据发送部分的代码。
//函数功能: 将发送位置调整为串口缓冲区的第一个待发送数据
//如果串口缓冲区已经有数据在发送,此时ser0.tx.len != 0, 则调用此函数无效
//如果串口缓冲区之前没有数据要发送,第一次调用此函数时,ser0.tx.len == 0,
//此时,将发送位置调整为串口发送缓冲区第一个待发送数据的位置
void serial_u0_send_start(void )
{
   if(ser0.tx.len != 0) return;
   ser0.tx.count = ser0.tx.pos;
}

//函数功能: 将十六进制数据复制到串口发送缓冲区,这个缓冲区是一个环形缓冲区,不必担心溢出问题
//ptxd --- 数据指针
//len  --- 数据长度
void serial_u0_send_hex(unsigned char *ptxd, unsigned char len)
{
   unsigned char i;
   
   serial_u0_send_start();
   
   for(i = 0 ; i < len; i++)
   {
        //ser0.tx.pos不用手动清零.比如 UART0_TX_BUF_COUNT == 0x0f(15),当
        //ser0.tx.pos累加到0x10时,(ser0.tx.pos & UART0_TX_BUF_COUNT) == 0,
        //此时,下一个数据将放在串口发送缓冲区索引为0的位置
       ser0.tx.fifo[ser0.tx.pos++ & UART0_TX_BUF_COUNT] = *ptxd++;
   }
   //累计串口发送缓冲区中待发送数据的总长度   
   ser0.tx.len += len;
}

//函数功能: 发送字符串
void serial_u0_send_str(unsigned char *ptxs)
{
   unsigned char len;
  
   len = strlen((char *)ptxs);
   serial_u0_send_hex(ptxs, len);
}

//函数功能: 将十六进制转换为对应的字符,然后复制到发送缓冲区。
//比如:
//要发送0xaa,这个函数的任务就是把0xaa转化为'a','a',' '(空格)
//三个字符,然后把这3个字符复制到发送缓冲区,这样,便于电脑上的
//串口软件以统一的ASC格式接收数据
void serial_u0_send_hex_char(unsigned char *ptxd,unsigned char len)
{
   serial_u0_send_start();
   
   for(unsigned char i = 0; i < len; i++)
   {
     ser0.tx.fifo[ser0.tx.pos++ & UART0_TX_BUF_COUNT] = char_tab[ptxd[i] >> 4];
     ser0.tx.fifo[ser0.tx.pos++ & UART0_TX_BUF_COUNT] = char_tab[ptxd[i]&0x0f];
     ser0.tx.fifo[ser0.tx.pos++ & UART0_TX_BUF_COUNT] = ' ';
   }
   
   ser0.tx.len += 3*len;
}

//函数功能: 串口发送缓冲区的管理函数,这个函数需要放在一个软件
//定时器中循环调用.这里串口发送数据采用的既不是查询也不是
//中断方式,而是采用时间间隔的方式发送.
//比如:
//串口的波特率是9600,那么串口发送一位的时间是104us,而一般
//串口一个字节要发送10位,一个字节在9600波特率下的传输时间
//至少是104us*10 = 1.04ms,因此,只要将这个函数放在定时间隔
//超过1.04ms的定时器中就可以安全发送串口数据,而不需要查询
//串口寄存器相关状态或者使用中断去发送数据.
//这样做的好处是发送程序更为简单,也更为通用,因为这跟硬件
//相关的寄存器以及中断无关.
void serial_u0_send_manage(void )
{
  if(ser0.tx.len)
  {
   //hal_uart0_set_tx_data()函数定义在硬件层(hal.c),作用就是把串口缓冲区的数据
   //给到串口寄存器,参考我之前写的“单片机程序架构---二层架构”一文.
    hal_uart0_set_tx_data(ser0.tx.fifo[ser0.tx.count++ & UART0_TX_BUF_COUNT]);
    ser0.tx.len--;
  }
}
我们再来看一下使用上述函数完成串口数据发送的例子。
#include "mid_serial.h"

unsigned char test_dat[5] = {0x01,0x02,0x03,0x04,0x05};

//1ms软件定时器,假设串口的波特率为9600
void app_sys_clk_1ms(void )
{
  static unsigned char clk = 0;
   
  if(++clk > 1)
  {
    clk = 0;
    serial_u0_send_manage()  
  } //2ms间隔
}

serial_u0_send_str("tx:");
//以十六进制字符格式发送test_dat
serial_u0_send_hex_char(test_dat,5);
//发送换行符
serial_u0_send_str("\r\n");
serial_u0_send_str("test");

//结果:
//:tx:01 02 03 04 05
//:test
最后,就是这个模块参数初始化相关的代码,这个很简单。
void serial_parameters_init(void )
{
    ser0.tx.fifo = u0_tx;
    ser0.rx.fifo = u0_rx;
}
这个函数需要在使用串口接收以及发送函数之前调用,一般放在串口硬件初始化的后面,当然你也可以放在它的前面,如下。
#include "mid_serial.h"
#include "hal.h"


main()
{
  //其他初始化函数  
  hal_uart_init();  
  serial_parameters_init();
  //其他代码
  while(1)
  {
    //其他代码
  }
}
到现在为止,串口模块部分就全部说完了。要说明的是,在使用这个模块时,考虑到移植或者程序的复杂度问题,并没有使用回调函数来隔离上下层。程序的上下层调用是通过直接调用上下层函数完成的,这也意味着上下层的隔离度并不是很好。但就如同我在“单片机程序架构---二层架构”中说的一样,程序的通用性总要兼顾实际情况,过分的追求通用性有时候也不见得是一件好事。

有了这个串口模块的准备,我们就可以正式的讲讲3层架构了。





使用特权

评论回复

相关帖子

沙发
会笑的星星|  楼主 | 2019-12-29 14:15 | 只看该作者

使用特权

评论回复
板凳
lihui567| | 2019-12-29 23:47 | 只看该作者
串口最为mcu非常常用的外设,楼主分析的很好,可以参考借鉴

使用特权

评论回复
地板
xuxg2012| | 2019-12-30 12:49 | 只看该作者
楼主,有联系方式吗

使用特权

评论回复
5
会笑的星星|  楼主 | 2019-12-30 13:10 | 只看该作者
xuxg2012 发表于 2019-12-30 12:49
楼主,有联系方式吗

有什么问题吗

使用特权

评论回复
6
xuxg2012| | 2019-12-30 13:54 | 只看该作者
想一块沟通交流,互通有无啊

使用特权

评论回复
7
xuxg2012| | 2019-12-30 13:55 | 只看该作者
写的很好

使用特权

评论回复
8
aerwa| | 2020-1-1 07:48 | 只看该作者
存道解惑,授人以渔。

使用特权

评论回复
9
qq5782098| | 2020-1-2 10:07 | 只看该作者
楼主,有没有这种书介绍下。

使用特权

评论回复
10
flame123| | 2020-1-2 10:10 | 只看该作者
楼主讲的挺好

使用特权

评论回复
11
yizushijie| | 2020-1-13 11:25 | 只看该作者
现在就被就是这样运用过的

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

31

主题

96

帖子

15

粉丝