查看: 989|回复: 0

[经验] 串口模块---讲三层架构前的铺垫

[复制链接]
  • TA的每日心情
    开心
    2019-11-4 13:48
  • 签到天数: 14 天

    连续签到: 1 天

    [LV.3]偶尔看看II

    发表于 2020-1-2 09:19:23 | 显示全部楼层 |阅读模式
    分享到:
    在说三层架构之前,先介绍一下串口模块的相关函数,这个模块把串口发送以及接收相关的功能给抽象出来了。我后面将以这个模块为例介绍设计三层架构的方法。之所以要以这个模块为例子,是因为如果介绍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

    [color=rgb(51, 102, 153) !important]复制代码

    • #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--;
    •   }
    • }


    [color=rgb(51, 102, 153) !important]复制代码

    下面举一个例子来说明如何使用上述的串口接收代码。
    • #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)
    • {
    • }

    [color=rgb(51, 102, 153) !important]复制代码

    串口接收部分就说完了,下面说一下串口数据发送部分的代码。
    • //函数功能: 将发送位置调整为串口缓冲区的第一个待发送数据
    • //如果串口缓冲区已经有数据在发送,此时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 >> 4];
    •      ser0.tx.fifo[ser0.tx.pos++ & UART0_TX_BUF_COUNT] = char_tab[ptxd&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--;
    •   }
    • }

    [color=rgb(51, 102, 153) !important]复制代码

    我们再来看一下使用上述函数完成串口数据发送的例子。
    • #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

    [color=rgb(51, 102, 153) !important]复制代码

    最后,就是这个模块参数初始化相关的代码,这个很简单。
    • void serial_parameters_init(void )
    • {
    •     ser0.tx.fifo = u0_tx;
    •     ser0.rx.fifo = u0_rx;
    • }

    [color=rgb(51, 102, 153) !important]复制代码

    这个函数需要在使用串口接收以及发送函数之前调用,一般放在串口硬件初始化的后面,当然你也可以放在它的前面,如下。
    • #include "mid_serial.h"
    • #include "hal.h"
    • main()
    • {
    •   //其他初始化函数
    •   hal_uart_init();
    •   serial_parameters_init();
    •   //其他代码
    •   while(1)
    •   {
    •     //其他代码
    •   }
    • }


    [color=rgb(51, 102, 153) !important]复制代码

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

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


    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    手机版|小黑屋|与非网

    GMT+8, 2024-4-24 12:18 , Processed in 0.106220 second(s), 15 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.