TA的每日心情 | 开心 2019-11-4 13:48 |
---|
签到天数: 14 天 连续签到: 1 天 [LV.3]偶尔看看II
|
在说三层架构之前,先介绍一下串口模块的相关函数,这个模块把串口发送以及接收相关的功能给抽象出来了。我后面将以这个模块为例介绍设计三层架构的方法。之所以要以这个模块为例子,是因为如果介绍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层架构了。
|
|