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

[复制链接]
2729|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中做详细解释。
  1. #ifndef _MID_UART_
  2. #define _MID_UART_

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

  4. #define UART_TX_BUF_LENGTH_1    0
  5. #define UART_TX_BUF_LENGTH_2    1
  6. #define UART_TX_BUF_LENGTH_4    3
  7. #define UART_TX_BUF_LENGTH_8   7
  8. #define UART_TX_BUF_LENGTH_16  15
  9. #define UART_TX_BUF_LENGTH_32  31
  10. #define UART_TX_BUF_LENGTH_64  63
  11. #define UART_TX_BUF_LENGTH_128 127
  12. #define UART_TX_BUF_LENGTH_256 255

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

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

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

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

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

  31. //发送十六进制数据
  32. extern void serial_u0_send_hex(unsigned char *ptxd, unsigned char len);
  33. //发送字符串
  34. extern void serial_u0_send_str(unsigned char *ptxs);
  35. //发送十六进制对应的字符格式数据
  36. extern void serial_u0_send_hex_char(unsigned char *ptxd,unsigned char len);
  37. //发送数据管理
  38. extern void serial_u0_send_manage(void );

  39. #endif
复制代码
  1. #include "mid_serial.h"

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

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

  15. struct
  16. {
  17.   rx_t rx;
  18.   tx_t tx;
  19. }ser0;

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

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

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

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

  2. //单片机的串口接收中断
  3. DEFINE_ISR(UART0_RX_ISR,0x10)
  4. {
  5.   if(_t1af)
  6.   {
  7.            _t1af = 0;
  8.           serial_u0_receiver_data(_txr_rxr); //_txr_rxr为单片机的串口接收寄存器
  9.   }
  10. }

  11. //软件定时器10ms
  12. void app_sys_clk_10ms(void )
  13. {
  14.    serial_u0_receiver_data_manage()
  15. }

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

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

  29. //函数功能: 发送字符串
  30. void serial_u0_send_str(unsigned char *ptxs)
  31. {
  32.    unsigned char len;
  33.   
  34.    len = strlen((char *)ptxs);
  35.    serial_u0_send_hex(ptxs, len);
  36. }

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

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

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

  3. //1ms软件定时器,假设串口的波特率为9600
  4. void app_sys_clk_1ms(void )
  5. {
  6.   static unsigned char clk = 0;
  7.    
  8.   if(++clk > 1)
  9.   {
  10.     clk = 0;
  11.     serial_u0_send_manage()  
  12.   } //2ms间隔
  13. }

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

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


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

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





使用特权

评论回复
 楼主 | 2019-12-29 14:15 | 显示全部楼层

使用特权

评论回复
| 2019-12-29 23:47 | 显示全部楼层
串口最为mcu非常常用的外设,楼主分析的很好,可以参考借鉴

使用特权

评论回复
| 2019-12-30 12:49 | 显示全部楼层
楼主,有联系方式吗

使用特权

评论回复
 楼主 | 2019-12-30 13:10 | 显示全部楼层
xuxg2012 发表于 2019-12-30 12:49
楼主,有联系方式吗

有什么问题吗

使用特权

评论回复
| 2019-12-30 13:54 | 显示全部楼层
想一块沟通交流,互通有无啊

使用特权

评论回复
| 2019-12-30 13:55 | 显示全部楼层
写的很好

使用特权

评论回复
| 2020-1-1 07:48 | 显示全部楼层
存道解惑,授人以渔。

使用特权

评论回复
| 2020-1-2 10:07 | 显示全部楼层
楼主,有没有这种书介绍下。

使用特权

评论回复
| 2020-1-2 10:10 | 显示全部楼层
楼主讲的挺好

使用特权

评论回复
| 2020-1-13 11:25 | 显示全部楼层
现在就被就是这样运用过的

使用特权

评论回复
扫描二维码,随时随地手机跟帖
您需要登录后才可以回帖 登录 | 注册

本版积分规则

我要发帖 投诉建议 创建版块 申请版主

快速回复

您需要登录后才可以回帖
登录 | 注册
高级模式

论坛热帖

关闭

热门推荐上一条 /2 下一条

在线客服 快速回复 返回顶部 返回列表