1.简介
驱动软件开发,就好比做菜,通过不同的烹饪方式,来做出满汉全席。
第一种方法就是用最简单的食材(GPIO),通过最高端的烹饪方式(模拟),将最鲜,最原始的味道(时序)来刺激我们的味蕾。
第二种方法则是往往使用丰富的食材,调料,添加到炒锅(高集成化的硬件控制器)中,通过最简单的烹饪方式(一件启动)完成美食的制作。
因此,上述的两种方法,在嵌入式驱动软件开发中,我们称之为GPIO模拟和硬件驱动控制器实现。
1.1 GPIO模拟和硬件控制器区别
(1)GPIO模拟是通过输出高低电平的时间,检测电平值和电平值的持续时间来实现例如UART,SPI,IIC等通信协议的时序,进而与其他外设通信。
(2)硬件控制器可以理解为更完备的GPIO模拟,是一种高度集成化的硬件电路。将UART,SPI,IIC的时序逻辑通过硬件实现。而软件只需要对其所在的控制器进行配置,从而达到与其他外设通信的目的。
两者的利弊:
GPIO模拟:
优势 | 劣势 |
通用性:可以模拟任意协议时序 | 时序准确性受CPU运行速率影响(延时,执行指令) |
学习性:易于理解和掌握不同的协议时序 | 输入/输出频率受限(高频设备无法驱动) |
时序模拟需严格按照协议要求进行编码,软件设计复杂 |
硬件驱动控制器:
优势 | 劣势 |
准确性:输入输出时序不受CPU影响,时序准确 | 不能直观的理解时序的控制逻辑 |
丰富性:不同的配置参数,可以配置不同的逻辑功能 | 单一性:每种控制器对应一种外设 |
完备性:协议逻辑完整,可应对不同的应用场景 | |
易用性:软件编码简单,只需对控制器进行配置 | |
封装性:集成度高,软件干预少 |
1.2 基本概念
由于控制器的优势所在,实际产品中,驱动软件开发主要基于硬件控制器来实现。而GPIO模拟更多的是作为辅助调试手段。因此本文只是针对控制器和本文的主要内容做阐述。
以下关键字的概念,需结合对应的数据手册作区分,这里只做参考和文章撰写。
(1)FIFO:(First-In-First-Out)先进先出,是一种数据结构或存储结构。数据的输入输出需遵循先入先出的原则。
(2)TX_TL:(Transmit FIFO Threshold Level)传输FIFO阈值数量,指传输FIFO中,已发送数量等于或少于该值时,触发对应事件。
(3)RX_TL:(Receive FIFO Threshold Level)接收FIFO阈值数量,指接收FIFO中,已接收数量等于或大于该值时,出发对应事件。
(4)TXFLR:(Transmit FIFO Level)传输FIFO数量,指传输FIFO中,存放的有效数据数量。
(5)RXFLR:(Receive FIFO Level)接收FIFO数量,指接收FIFO中,已接收的有效数据数量。
(6)控制器中断:指的是不同的控制器,在发送或接收过程中,产生相对应的中断事件。例如TX_EMPTY(发送阈值中断),RX_FULL(接收阈值中断),ABORT(传输终止)等。
2. FIFO逻辑
在控制器中(数字电路),FIFO缓冲区作为跨时钟域的数据传输,确保数据同步。
即:
(1)软件通过CPU的工作时钟对FIFO进行数据读写
(2)外设控制器通过器对应的工作时钟对FIFO进行数据读写
2.1 FIFO术语
(1)width(数据位宽):一般为8,16,32,64,......;其中,特殊的外设控制器可以配置实际的传输位数,例如SPI。
(2)depth(深度):指的是FIFO容量,一次性可以存放的数据个数,一般为8,16,32。
2.2 FIFO形式
如上述的图过程:
根据FIFO的depth,将一定数量的数据填充到FIFO中,当有读请求过程时,会将先写入的数据读出,之后如果有数据需要写入,则向后填写。
2.3 FIFO数量处理
关于FIFO内部数量,需要结合其depth,考虑以下几个问题:
(1)数据是否溢出?
(2)何时进行数据的读写?
(3)TX_TL、RX_TL如何设置?
以FIFO的depth=8为例:
(1)关于数据是否会溢出,关键在于FIFO当前可写入数量。
A.第一种情况:显而易见,如果单次传输的数量大于8,则数据溢出。
B.第二种情况:中断事件中,如果FIFO中存在数据未发送完毕,一次性写入数量大于8,数据也会溢出。
(2)数据的读写,包括主动查询和被动触发两种方式:
A.主动查询:通过查询FIFO状态(FIFO是否为空,FIFO是否已满,FIFO中的有效数据量等)来进行数据的收发。
B.被动触发:结合TX_TL、RX_TL触发中断事件(TX_EMPTY,RX_FULL),来进行后续数据的收发处理。
(3)TX_TL、RX_TL由两个因素决定:
A.数据量:指传输个数远超过FIFO深度
B.单次传输的个数:指一次性读写FIFO的个数,目的是提高传输效率
接下来,以具体的过程来描述TX_TL、RX_TL和FIFO之间的逻辑关系:
发送过程:假定TX_TL配置为4,总共发送7个数据
过程如下:
(1)向FIFO写入4个数据,等待读请求。
(2)数据读完毕后,触发TX_EMPTY事件.
(3)继续向FIFO写入3个数据,等待发送完毕,再次触发TX_EMPTY事件。
(4)程序判断是否结束。
接收过程:假定RX_TL配置为2,总共接收5个数据
过程如下:
(1)当外部写入RX FIFO 2个数据时,触发RX_FULL事件。
(2)程序读取数据,并读空FIFO。
(3)外部再次写入3个数据,触发RX_FULL事件。
(4)程序继续读取数据并判断是否结束。
需要注意的是:接收到的数据个数需大于等于RX_TL,才能触发RX_FULL事件。
3. FIFO模式下的中断处理
前面提到,无论是发送还是接收模式,再设定的TX_TL、RX_TL条件下,会触发对应的事件。而这个事件在微控制器系统中,一般指的是中断。驱动软件中,一旦开启了控制器对应的中断,当触发中断事件时,则需要根据配置的参数做对应的处理。
接下来,就发送,接收,发送接收模式做相关的逻辑说明。
3.1 发送模式
该模式下的TX_EMPTY中断服务程序,需要根据TX_TL,FIFO状态,数量做逻辑处理。包括两种设计思路:
(1)当触发TX_EMPTY中断时,根据TX_TL,FIFO深度以及发送数量做处理。步骤如下:
A.计算剩余发送个数
B.计算填写FIFO的个数(根据TX_TL depth),需考虑是否溢出
C.循环写入FIFO
D.等待下一次中断
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//变量
static unsigned int tx_total_cnt;
static volatile unsigned int tx_cnt = 0;
static unsigned char *tx_data;
//发送函数
void xxx_send(const void *tx, unsigned int num)
{
//初始化数据
tx_data = (unsigned char*)tx;
tx_total_cnt = num;
tx_cnt = 0;
//使能中断
//启动控制器发送
}
void isr_tx_handle()
{
unsigned int i = 0, tx_remain = 0, w_cnt;
//判断数据是否发送完毕
if(tx_cnt == tx_total_cnt)
{
//do something
return;
}
//计算剩余发送个数
tx_remain = tx_total_cnt - tx_cnt;
//计算写入FIFO个数,通过 深度-阈值,避免溢出
w_cnt = (tx_remain > (depth - TX_TL))?(depth - TX_TL):tx_remain;
//循环写入FIFO
for(i=0; i<w_cnt; i++)
{
//假定FIFO寄存器为DR
DR = *data++;
tx_cnt++;
}
}
(2)当触发TX_EMPTY中断时,根据FIFO状态和数量做处理。步骤如下:
A.判断数据是否发送完成
B.判断FIFO是否写满
C.循环写入FIFO
D.等待下一次中断
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//变量
static unsigned int tx_total_cnt;
static volatile unsigned int tx_cnt = 0;
static unsigned char *tx_data;
//发送函数
void xxx_send(const void *tx, unsigned int num)
{
//初始化数据
tx_data = (unsigned char*)tx;
tx_total_cnt = num;
tx_cnt = 0;
//使能中断
//启动控制器发送
}
//FIFO是否未满
bool xxx_tx_full_is_not_full()
{
//假定FIFO状态寄存器为SR,tx full位于bit3
if((SR & 0x8) == 0)
return false;
return true;
}
void isr_tx_handle()
{
//判断数据是否发送完毕
if(tx_cnt == tx_total_cnt)
{
//do something
return;
}
//判断FIFO状态和已发送个数
while((tx_cnt < tx_total_cnt) && (xxx_tx_full_is_not_full))
{
//循环写入FIFO
//假定FIFO寄存器为DR
DR = *data++;
tx_cnt++;
}
}
两种设计思路差异:
第一种:可以由软件直接控制FIFO个数的写入和中断触发的次数,但是需要准确控制写入的个数
第二种:无需关心写入FIFO的数量,有空间则写入;但该方式不能直观的看到中断触发的次数,且占用CPU的时间相对较长。
3.2 接收模式
该模式下的RX_FULL中断服务程序,需要根据RX_TL,RXFLR,数量做逻辑处理。步骤如下:
(1)根据实际的单次传输数量设定RX_TL值
(2)等待RX_RULL中断
(3)结合RXFLR和数量读取数据
(4)程序判断是否结束
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//变量
static unsigned int rx_total_cnt;
static volatile unsigned int rx_cnt = 0;
static unsigned char *rx_data;
//发送函数
void xxx_recv(void *rx, unsigned int num)
{
//初始化数据
rx_data = (unsigned char*)rx;
rx_total_cnt = num;
rx_cnt = 0;
//使能中断
//启动控制器接收
}
void isr_rx_handle()
{
//判断数据是否接收完毕
if(rx_cnt == rx_total_cnt)
{
//do something
return;
}
//判断FIFO状态和已接收个数
while((rx_cnt < rx_total_cnt) && (RXFLR > 0))
{
//循环读取FIFO
//假定FIFO寄存器为DR
*data++ = DR;
rx_cnt++;
}
}
备注:如果控制器没有RXFLR,则可以根据RX_TL的进行数据读取,类似发送模式的方法。
3.3 发送接收模式
该模式主要应用于全双工的处理逻辑,会同时存在发送和接收两种,则需要根据TX_TL,RX_TL,FIFO状态,RXFLR,数量等多个参数进行处理,即需要结合发送和接收两种模式。步骤如下:
(1)等待TX_EMPTY中断触发
(2)判断发送的数据个数,写入TX FIFO
(3)等待RX_FULL中断触发
(4)读取RX FIFO数据
(5)判断程序是否结束
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//变量
//发送
static unsigned int tx_total_cnt;
static volatile unsigned int tx_cnt = 0;
static unsigned char *tx_data;
//接收
static unsigned int rx_total_cnt;
static volatile unsigned int rx_cnt = 0;
static unsigned char *rx_data;
//发送函数
void xxx_transfer(const void *tx, void *rx, unsigned int num)
{
//初始化数据
tx_data = (unsigned char *)tx;
tx_total_cnt = num;
tx_cnt = 0;
rx_data = (unsigned char *)rx;
rx_total_cnt = num;
rx_cnt = 0;
//使能中断
//启动控制器接收
}
//先发后收
void isr_rx_transfer()
{
//判断数据是否接收完毕,因接收和发送的数量应该保持一致,这里判断接收即可
if(rx_cnt == rx_total_cnt)
{
//do something
return;
}
//RX_FULL中断 判断FIFO状态和已接收个数
while((rx_cnt < rx_total_cnt) && (RXFLR > 0))
{
//循环读取FIFO
//假定FIFO寄存器为DR
*data++ = DR;
rx_cnt++;
}
//TX_EMPTY中断,填写FIFO
unsigned int i = 0, tx_remain = 0, w_cnt;
//计算剩余发送个数
tx_remain = tx_total_cnt - tx_cnt;
//计算写入FIFO个数,通过 深度-阈值,避免溢出
w_cnt = (tx_remain > (depth - TX_TL))?(depth - TX_TL):tx_remain;
//循环写入FIFO
for(i=0; i<w_cnt; i++)
{
//假定FIFO寄存器为DR
DR = *data++;
tx_cnt++;
}
}
4.总结
本文主要介绍的是在FIFO模式下的中断处理逻辑和思维方式。在实际开发过程中,不同的驱动控制器的设计及其它的复杂性都有所不同,因此,我们需要结合其对应的databook进行开发调试。不过,值得肯定的是,其中最基本的原理都是相通的。望驱动软件之美,你我他皆知!!