硬件工作模式選擇分析
在serial_v2中,串口設備以應用層視角,即阻塞模式或非阻塞模式來作為該串口設備的開啟標志.
// 使用阻塞接收阻塞發送模式開啟uart_dev設備
rt_device_open(uart_dev,RT_DEVICE_FLAG_TX_BLOCKING|RT_DEVICE_FLAG_TX_BLOCKING);
以下為所有的設備開啟模式選擇:
#define RT_DEVICE_FLAG_RX_BLOCKING
#define RT_DEVICE_FLAG_RX_NON_BLOCKING
#define RT_DEVICE_FLAG_TX_BLOCKING
#define RT_DEVICE_FLAG_TX_NON_BLOCKING
對于其在底層傳輸中到底是采用輪詢,中斷,還是DMA,則取決于在rtconfig中對發送與接收緩沖區以及DMA相關的宏進行配置.
//開啟DMA
#define RT_SERIAL_USING_DMA
//將串口1的發送緩沖區與接收緩沖區的大小設置為128
#define BSP_UART1_RX_BUFSIZE 128
#define BSP_UART1_TX_BUFSIZE 128
serial_v2串口設備框架對底層硬件工作模式的選擇原則大致可總結為:
(該原則對于TX與RX通用 )
緩沖區大小被設置為0,則使用輪詢模式.
若開啟DMA,緩沖區大小不為0,則優先使用DMA模式。
若未開啟DMA,且緩存又并非為0,則使用中斷模式。
接下來,筆者將詳細分析,串口設備框架與驅動是如何選擇硬件工作模式的.
在RT-Thread中,如果我們想使用串口,那我們的第一步將會是:
//假設以阻塞接收和阻塞發送模式打開串口
rt_device_open(uart_dev,RT_DEVICE_RX_BLOCKING|RT_DEVICE_TX_BLOCKING)
阻塞模式作為一種應用層的視角,其底層可以由輪詢,中斷,或DMA中任意一種硬件工作模式實現.
在serial_v2中,rt_device_open()函數將會調用框架層的rt_serial_init()與rt_serial_open()函數,從而根據已有的配置,從三種硬件工作模式中選擇其中一種。
//串口初始化函數
//先將發送與接收緩沖區初始化為NULL, 然后對波特率等參數進行設置
static rt_err_t rt_serial_init(struct rt_device * dev)
{
...
//先發送緩沖區與接收緩緩沖區賦值為空,方便日后初始化
serial->serial_rx = RT_NULL;
serial->serial_tx = RT_NULL;
...
if (serial->ops->configure)
//此次跳轉到驅動層的函數,對波特率等參數進行配置
result = serial->ops->configure(serial, &serial->config);
}
//串口開啟函數, 主要調用rt_serial_rx_enable與rt_serial_tx_enable函數會對硬件工作模式做出選擇
static rt_err_t rt_serial_open(struct rt_device *dev, rt_uint16_t oflag)
{
//根據設備開啟模式,對RX進行初始化
if (serial->serial_rx == RT_NULL)
rt_serial_rx_enable(dev, dev->open_flag &(RT_SERIAL_RX_BLOCKINGRT_SERIAL_RX_NON_BLOCKING));
//根據設備開啟模式,對TX進行初始化
if (serial->serial_tx == RT_NULL)
rt_serial_tx_enable(dev, dev->open_flag & (RT_SERIAL_TX_BLOCKING|RT_SERIAL_TX_NON_BLOCKING));
}
接下來我們先具體分析一下在rt_serial_open()中調用的rt_serial_rx_enable()如何選擇RX硬件工作模式
static rt_err_t rt_serial_rx_enable(struct rt_device * dev, rt_uint16_t rx_oflag) {
// 第一種選擇:如果接收緩沖區的大小為0,則首先使用輪詢接收
if (serial->config.rx_bufsz == 0) {
...
//將設備的接收函數指針指向輪詢接收
dev->read = _serial_poll_rx;
dev->open_flag |= RT_SERIAL_RX_BLOCKING;
...
}
//為接收緩沖區分配內存,其緩沖區大小為在rtconfig中進行設置
rx_fifo = (struct rt_serial_rx_fifo * ) rt_malloc
(sizeof(struct rt_serial_rx_fifo) + serial->config.rx_bufsz);
//初始化完成量,用于阻塞等待
rt_completion_init(&(rx_fifo->rx_cpt));
//調用 control(),根據已有的配置,決定使用DMA或中斷
serial->ops->control(serial,RT_DEVICE_CTRL_CONFIG, (void *) RT_SERIAL_RX_BLOCKING )
}
serial->ops->control會調用驅動層的stm32_control(),對DMA或中斷進行選擇,筆者僅將關鍵部分列出 :
static rt_err_t stm32_control(struct rt_serial_device *serial,
int cmd, void arg)
{
//RX模式下的工作模式選擇
if(ctrl_arg&( RT_DEVICE_FLAG_RX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING))
{
...
//先判斷DMA相關的宏是否被定義,如果是則將ctrl_arg賦值為DMA相關標志,為下文初始化做準備
if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_RX)
ctrl_arg = RT_DEVICE_FLAG_DMA_RX;
else
ctrl_arg = RT_DEVICE_FLAG_INT_RX;
...
}
...
//如果ctrl_arg被賦予了DMA標志相關的值,則選擇DMA模式,對DMA進行初始化
if (ctrl_arg & (RT_DEVICE_FLAG_DMA_RX | RT_DEVICE_FLAG_DMA_TX)) {
#ifdef RT_SERIAL_USING_DMA
//具體分析見下文
stm32_dma_config(serial, ctrl_arg);
}
else
{
//如果未開啟DMA.則選擇中斷模式,則對中斷相關的函數進行初始化
//具體分析見下文
stm32_control(serial, RT_DEVICE_CTRL_SET_INT, (void )ctrl_arg);
break;
}
}
stm32_control()對中斷接收進行初始化:
static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg)
{
...
case RT_DEVICE_CTRL_SET_INT:
//設置單個中斷搶占優先級和響應優先級
HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0);
//設置使能中斷通道
HAL_NVIC_EnableIRQ(uart->config->irq_type);
//使能RXNE相關中斷
if (ctrl_arg == RT_DEVICE_FLAG_INT_RX)
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_RXNE);
break;
}
stm32_dma_config()對DMA接收進行初始化
static void stm32_dma_config(struct rt_serial_device *serial, rt_ubase_t flag)
{
...
if (RT_DEVICE_FLAG_DMA_RX == flag)
{
//設置DMA接收數據所用的RX緩沖區
DMA_Handle = &uart->dma_rx.handle;
dma_config = uart->config->dma_rx;
...
if (RT_DEVICE_FLAG_DMA_RX == flag)
{
__HAL_LINKDMA(&(uart->handle), hdmarx, uart->dma_rx.handle);
}
}
...
//使能DMA相關中斷
if (flag == RT_DEVICE_FLAG_DMA_RX)
{
rx_fifo = (struct rt_serial_rx_fifo )serial->serial_rx;
RT_ASSERT(rx_fifo != RT_NULL);
/ Start DMA transfer /
if (HAL_UART_Receive_DMA(&(uart->handle), rx_fifo->buffer, serial->config.rx_bufsz) != HAL_OK)
{
/ Transfer error in reception process */
RT_ASSERT(0);
}
CLEAR_BIT(uart->handle.Instance->CR3, USART_CR3_EIE);
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_IDLE);
}
...
//此處省略其他的初始化函數
}
以上就是RX的硬件工作模式選擇,與對應硬件初始化的部分代碼
接下來分析一下rt_serial_tx_enable()如何選擇硬件工作模式與初始化硬件:
static rt_err_t rt_serial_tx_enable(struct rt_device*dev, rt_uint16_t rx_oflag){
// 第一種情況:如果發送緩沖區的大小為0,則使用輪詢發送
if (serial->config.tx_bufsz == 0)
{
...
dev->read = _serial_poll_tx;
dev->open_flag |= RT_SERIAL_TX_BLOCKING;
...
}
//如果是阻塞模式,接下來就將調用底層control函數,選擇DMA或中斷
if (tx_oflag == RT_SERIAL_TX_BLOCKING) {
optmode = serial->ops->control(serial,RT_DEVICE_CHECK_OPTMODE,
(void *)RT_DEVICE_FLAG_TX_BLOCKING);
*補充 : serial->ops->control()函數的源碼
*即如何在DMA與中斷中做出選擇
*RT_DEVICE_CHECK_OPTMODE:
- {
//如果開啟DMA,那么優先使用DMA
if (ctrl_arg & RT_DEVICE_FLAG_DMA_TX)
return RT_SERIAL_TX_BLOCKING_NO_BUFFER;
else
//未開啟則使用中斷
return RT_SERIAL_TX_BLOCKING_BUFFER;
- }
//optmode的值,即上述調用函數的返回值有兩種情況:
//RT_SERIAL_TX_BLOCKING_BUFFER,使用框架層緩沖區,即中斷發送,
//RT_SERIAL_TX_BLOCKING_NO_BUFFER,不使用框架層緩沖區,即使用DMA發送
//中斷與DMA發送在軟件層面的一大區別是中斷發送需要使用框架層的發送緩沖區而DMA發送直接使用應用層緩沖區,不需要框架層分配緩沖區
if (optmode == RT_SERIAL_TX_BLOCKING_BUFFER){
//使用中斷發送
//則首先為tx緩沖區分配有tx_bufsz大小的空間
tx_fifo = (struct rt_serial_tx_fifo ) rt_malloc
(sizeof(struct rt_serial_tx_fifo)+serial->config.tx_bufsz);
//將設備的寫函數指針指向中斷發送函數
dev->write = _serial_fifo_tx_blocking_buf;
...
}
else
{
//使用DMA模式發送,此處緩沖區沒有分配tx_bufsz大小的空間
//DMA使用的是應用層的buffer
tx_fifo = (struct rt_serial_tx_fifo ) rt_malloc
(sizeof(struct rt_serial_tx_fifo));
//初始化DMA
serial->ops->control(serial,RT_DEVICE_CTRL_CONFIG,
(void *)RT_SERIAL_TX_BLOCKING);
//因為使用不使用框架層接收緩沖區,因此將其設置為NULL
rt_memset(&tx_fifo->rb, RT_NULL, sizeof(tx_fifo->rb));
...
}
//將激活標識設置為false,激活標識用于標記發送緩存是否被使用
tx_fifo->activated = RT_FALSE;
//阻塞發送需要初始化完成量
rt_completion_init(&(tx_fifo->tx_cpt));
...
}
}
stm32_control對中斷發送進行初始化:
static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg)
{
...
case RT_DEVICE_CTRL_SET_INT:
//設置單個中斷搶占優先級和響應優先級
HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0);
//設置使能中斷通道
HAL_NVIC_EnableIRQ(uart->config->irq_type);
//使能TXE相關中斷
if (ctrl_arg == RT_DEVICE_FLAG_INT_TX)
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TXE);
break;
}
stm32_dma_config對DMA發送進行初始化:
static void stm32_dma_config(struct rt_serial_device *serial, rt_ubase_t flag)
{
...
else //RT_DEVICE_FLAG_DMA_TX == flag
{
//對相關句柄進行賦值
DMA_Handle = &uart->dma_tx.handle;
dma_config = uart->config->dma_tx;
}
...
if (RT_DEVICE_FLAG_DMA_TX == flag)
{
//設置好DMA的發送緩沖區
__HAL_LINKDMA(&(uart->handle), hdmatx, uart->dma_tx.handle);
}
...
//此處省略其他的初始化函數
}
目前通過分析,我們理清楚了串口設備框架是如何與驅動合作,對底層的硬件工作方式,根據已有的配置做出選擇,并對選擇的硬件工作方式進行相關的初始化工作,接下來,我們將分析serial_v2在阻塞模式下的具體工作流程.
阻塞模式
阻塞模式下的數據接收
數據接收函數分析
static rt_ssize_t rt_serial_read(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
...
//如果設置了接收緩沖區,則調用_serial_fifo_rx(),即使用中斷或DMA模式接收
if (serial->config.rx_bufsz)
{
return _serial_fifo_rx(dev, pos, buffer, size);
}
//否則緩沖區大小為0,使用輪詢模式,此處結論與上文一致
return _serial_poll_rx(dev, pos, buffer, size);
}
輪詢
輪詢,就是CPU通過不斷地查詢某個外部設備的狀態,如果外部設備準備好,就可以向其發送數據或者讀取數據。然而,這種方式由于CPU不斷查詢總線,導致指令執行受到影響,效率較低。 以下是serial_v2中輪詢_serial_poll_rx()函數的部分源碼.
//驅動框架層函數
rt_ssize_t _serial_poll_rx(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size) {
...
while(size)
{
//調用驅動層函數去讀一個數據
getc_element = serial->ops->getc(serial);
if (getc_element == -1) break;
*getc_buffer = getc_element;
++ getc_buffer;
-- size;
}
return getc_size - size;
}
其中serial->ops->getc(serial)在底層驅動中調用stm32_getc()
以下為stm32_getc()部分源碼
//驅動層函數
static int stm32_getc(struct rt_serial_device *serial){
...
ch = -1;
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET)
ch = UART_GET_RDR(&uart->handle,stm32_uart_get_mask(...));
return ch;
}
通過以上代碼,我們可以發現,輪詢的底層實現就是就是不斷檢查RXNE(接收數據寄存器非空標志位),然后從RDR(接收數據寄存器)中讀取數據,如果某一次RXNE寄存器被復位,那么_serial_poll_rx()函數會直接返回已經讀到的數據的數量,結束輪詢接收。
中斷
中斷方式克服了CPU輪詢外部設備的缺點,正常情況下,CPU執行指令,不會主動去檢查外部設備的狀態。外部設備準備好之后,向CPU發送中斷信號,CPU收到中斷信號后停止當前的工作,會根據中斷信號指定的設備號處理相應的設備。這種處理方式既不影響CPU的工作,也能保證外部設備的數據得到及時處理,工作效率很高.
由上文rt_serial_read()函數分析可知,中斷接收是由_serial_fifo_rx(dev, pos, buffer, size)實現的。
//驅動框架層中斷接收部分代碼
static rt_ssize_t _serial_fifo_rx(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
if (dev->open_flag & RT_SERIAL_RX_BLOCKING)
{
...
//此處可以注意到,阻塞接收函數第一步是檢查應用層想讀的數據量是否大于緩存,
//如果大于,函數將不工作,直接返回,并提示需要改變緩沖區大小設置
if (size > serial->config.rx_bufsz)
{
LOG_W("(%s) serial device received data:[%d] larger than " "rx_bufsz:[%d], please increase the BSP_UARTx_RX_BUFSIZE option",dev->parent.name, size, serial->config.rx_bufsz);
return 0;
}
//計算中斷接收到緩沖區的字節數量
recv_len = rt_ringbuffer_data_len(&(rx_fifo->rb));
//如果收到的數據小于應用層要求的數據,那么調用rt_completion_wait陷入等待
//直至有足夠的數據才退出
if (recv_len < size)
{
rx_fifo->rx_cpt_index = size;
//與下文中斷服務函數中完成量的喚醒遙相呼應,從而在應用層視角看是以阻塞模式進行數據接收
rt_completion_wait(&(rx_fifo->rx_cpt),RT_WAITING_FOREVER);
}
//關中斷,確保多線程下不會出現競態條件
level = rt_hw_interrupt_disable();
//將緩沖區的數據轉移到應用層緩沖區
recv_len = rt_ringbuffer_get(&(rx_fifo->rb), buffer, size);
rt_hw_interrupt_enable(level);
}
}
看到這里可能有人會問,怎么數據突然就跑到接收緩沖區了,數據是在什么時候被放進去的,詳細的流程是什么?接下來,筆者就通過一個場景以及相關的代碼大致來說明中斷接收的整個流程.
說明:
RDR: RX Data Register 接收數據寄存器
RXNE: RX Data Register Not Empty 接收數據寄存器為空
uart_isr() : 驅動層中斷服務函數
rt_hw_serial_isr() : 驅動框架層中斷服務函數
rx_fifo: 接收緩沖區
假設接收緩沖區大小為128,即在etconfig中設置#define BSP_UART1_RX_BUFSIZE 128
調用rt_device_open(uart_dev, RT_SERIAL_RX_BLOCKING),與rt_device_read(uart_dev,buffer,128),即應用層一次讀取128個數據
用戶輸入一段字符
設備收到字符觸發RXNE中斷
驅動層中斷服務函數uart_isr()接收RXNE中斷,將字符傳入緩沖區,并調用rt_hw_serial_isr()
框架層中斷服務函數rt_hw_serial_isr()根據rx_fifo接收的數據量,來判斷阻塞接收是否結束,若接收足夠的數據則喚醒完成量,并設置回調函數的參數
若rx_fifo中未獲取足夠的數據,重復4 - 6步,直至接收完成,此時rt_device_read()函數結束,返回接收字節數量.
//驅動層中斷服務函數
static void uart_isr(struct rt_serial_device *serial)
{
...
//如果RXNE未被復位,且接收到RXNE中斷,則從RDR寄存器中取數據
if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
{
//調用環形緩沖區ringbuff的putchar(),將讀取的一個字節放入ringbuffer
rt_ringbuffer_putchar(&(rx_fifo->rb), UART_GET_RDR(&
uart->handle, stm32_uart_get_mask(...)));
//調用設備框架層中斷服務函數,做后續的處理,決定讀取是否完成,見下一個代碼塊
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
}
...
}
//框架層中斷服務函數
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
...
case RT_SERIAL_EVENT_RX_IND:
//計算RX緩沖區的數據量
rx_length = rt_ringbuffer_data_len(&rx_fifo->rb);
if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
{
//如果ringbuffer緩沖區中有足夠的數據,那么可以調用完成量的喚醒函數
//喚醒上文中rt_serial_read()中等待的完成量
if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index)
{
rx_fifo->rx_cpt_index = 0;
//代表此次數據接收完成
//此處的喚醒與上文完成量的等待遙相呼應
rt_completion_done(&(rx_fifo->rx_cpt));
}
if (serial->parent.rx_indicate != RT_NULL)
//為回調函數函數設置參數
serial->parent.rx_indicate(&(serial->parent), rx_length);
}
}
總結,驅動層保證數據從RDR寄存器到rx_fifo,即接收緩沖區,框架層則統計tx_fifo中的數據是否符合應用層要求,,如果符合要求,則將數據復制到應用層緩沖區,二者有機結合,形成阻塞讀取.
DMA
中斷方式效率雖然很高,但是對于大量數據的傳輸就顯得力不從心。大量的中斷會導致CPU忙于處理中斷而減少對指令的處理,效率會變的很低。對于大量的數據傳輸可以不通過CPU而直接傳送到內存,這種方式叫做DMA(DIrect Memory Access)。使用DMA方式,外部設備在數據準備好之后只需向DMA控制器發送一個命令,把數據的地址和大小傳送過去,由DMA控制器負責把數據從外部設備直接存放到內存因此,DMA方式適合處理大量的數據。
由上文rt_serial_read()函數分析可知,DMA接收是由_serial_fifo_rx(dev, pos, buffer, size)實現的.雖然DMA模式接收的串口設備框架代碼與中斷接收一致,但是在中斷服務函數部分還是有很多不同.
//驅動框架層函數
static rt_ssize_t _serial_fifo_rx(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
//中斷接收部分代碼
if (dev->open_flag & RT_SERIAL_RX_BLOCKING)
{
...
//此處可以注意到,阻塞接收的函數第一步是檢查應用層想讀的數據量是否大于緩存,
//如果大于,函數將直接返回,并提示需要改變緩沖區大小設置
if (size > serial->config.rx_bufsz)
{
LOG_W("(%s) serial device received data:[%d] larger than " "rx_bufsz:[%d], please increase the BSP_UARTx_RX_BUFSIZE option",dev->parent.name, size, serial->config.rx_bufsz);
return 0;
}
//計算中斷接收到緩沖區的字節數量
recv_len = rt_ringbuffer_data_len(&(rx_fifo->rb));
//如果收到的數據小于應用層要求的數據,那么調用rt_completion_wait陷入等待
//直至有足夠的數據才退出
if (recv_len < size)
{
rx_fifo->rx_cpt_index = size;
//與下文中斷服務函數中完成量的喚醒遙相呼應,從而在應用層視角看是以阻塞模式進行數據接收
rt_completion_wait(&(rx_fifo->rx_cpt), RT_WAITING_FOREVER);
}
//關中斷,確保多線程下不會出現競態條件
level = rt_hw_interrupt_disable();
//將緩沖區的數據轉移到應用層緩沖區
recv_len = rt_ringbuffer_get(&(rx_fifo->rb), buffer, size);
rt_hw_interrupt_enable(level);
}
}
同樣,我們再次通過一個場景以及相關的代碼大致來說明DMA接收的整個流程
說明:
IDLE: 檢測到總線空閑中斷
uart_isr() : 驅動層中斷服務函數
dma_recv_isr() : 驅動層DMA接收中斷處理函數
rt_hw_serial_isr() : 驅動框架層中斷服務函數
rx_fifo: 接收緩沖區
調rt_device_open(),rt_device_read()
DMA外設讀取用戶輸入的一批字符
DMA將字符放入緩沖區后觸發IDLE中斷
中斷服務函數uart_isr()接收IDLE中斷,調用dma_recv_isr()計算接收字符數量
dma_recv_isr()最后調用rt_hw_serial_isr(),根據緩沖區數據數量來決定是否喚醒完成量,即如果數據不夠則不喚醒,繼續接收數據,最后設置回調函數的參數
重復2 - 5步, 直至接收完成,此時rt_device_read()中的完成量被喚醒,讀取函數結束,返回接收字節數量
static void uart_isr(struct rt_serial_device *serial)
{
#ifdef RT_SERIAL_USING_DMA
//查看IDLE中斷是否發生
if ((uart->uart_dma_flag)&&(__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET)&&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_IDLE) != RESET))
{
//調用設備框架層中斷處理函數
dma_recv_isr(serial, UART_RX_DMA_IT_IDLE_FLAG);
__HAL_UART_CLEAR_IDLE_FLAG(&uart->handle);
}
#endif
}
static void dma_recv_isr(struct rt_serial_device *serial,
rt_uint8_t isr_flag) {
...
//省略計算接收數據量的過程
if (recv_len) {
//將接收的數據量一同發往串口設備框架層
uart->dma_rx.remaining_cnt = counter;
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE
| (recv_len << 8));
}
...
}
//串口設備框架層中斷服務函數
void rt_hw_serial_isr(struct rt_serial_device * serial, int event) {
//獲取DMA接收的數據量
rx_length = (event & (~0xff)) >> 8;
if (rx_length)
{
//關中斷,避免并發bug
level = rt_hw_interrupt_disable();
//更新環形緩沖區的索引
rt_serial_update_write_index(&(rx_fifo->rb),rx_length);
//更新完成后再開中斷
rt_hw_interrupt_enable(level);
}
if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING)
{
//如果緩沖區數據足夠則喚醒完成量
if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index )
{
rx_fifo->rx_cpt_index = 0;
//喚醒完成量
rt_completion_done(&(rx_fifo->rx_cpt));
}
}
}
總結,DMA接收與中斷接收很多代碼是復用的,其思路與中斷接收及其相似,但在接收數據方面,DMA獨立自行接收數據,不需要像中斷接收一樣,每次讀取數據都占用CPU,從RDR寄存器拿到數據,再放入緩沖區中.在DMA模式下,中斷處理程序接收到TC中斷時,數據已然在接收緩沖區中.因此DMA接收的效率比中斷更高.
阻塞模式下的數據發送
數據發送函數分析
數據發送也遵循最開始提出的硬件工作模式選擇,即緩沖區為0則使用輪詢,開啟DMA則優先使用DMA,否則使用中斷.
static rt_ssize_t rt_serial_write(struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
...
if (serial->config.tx_bufsz == 0)
{
//發送緩沖區為0則,使用輪詢
return _serial_poll_tx(dev, pos, buffer, size);
}
if (dev->open_flag & RT_SERIAL_TX_BLOCKING)
{
//如果tx緩沖區為空指針,則使用DMA發送
//因為DMA發送只需要應用層緩沖區,不需要框架層緩沖區
if ((tx_fifo->rb.buffer_ptr) == RT_NULL)
{
return _serial_fifo_tx_blocking_nbuf(dev, pos, buffer, size);
}
//需要用到框架層緩沖區,則使用中斷發送
return _serial_fifo_tx_blocking_buf(dev, pos, buffer, size);
}
...
}
輪詢
//框架層輪詢發送函數
rt_ssize_t _serial_poll_tx(struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
...
//核心代碼
while (size)
{
//調用驅動層函數,一次將一個字節發送出去,詳解見下文
serial->ops->putc(serial, *putc_buffer);
++ putc_buffer;
-- size;
}
return putc_size - size;
}
//驅動層函數
//serial->ops->putc實際為串口驅動中的stm32_putc函數:
static int stm32_putc(struct rt_serial_device *serial, char c)
{
//重置傳輸完成中斷標志(ISR寄存器中)
UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
//將一個字節的數據放入TDR寄存器中
UART_SET_TDR(&uart->handle, c);
//陷入等待,直到獲取到傳輸完成標志位(TC),此處體現了阻塞發送,即未發送完成則一直等待
while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
return 1;
}
中斷
//框架層中斷發送函數
static rt_ssize_t _serial_fifo_tx_blocking_buf(
struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
...
//當激活標識為TRUE,代表tx_fifo即發送緩沖區正在被使用,因此直接返回0,不發送數據
if (tx_fifo->activated == RT_TRUE) return 0;
//之前tx_fifo未被使用,則現在將其設置為TRUE,表示被當前發送線程中的中斷發送占用
tx_fifo->activated = RT_TRUE;
//阻塞發送的核心代碼
while (size)
{
//將應用層中的數據先復制一份到發送緩沖區中
tx_fifo->put_size = rt_ringbuffer_put(&(tx_fifo->rb),
(rt_uint8_t *)buffer + offset,size);
//調用底層驅動的傳輸函數,發送數據
serial->ops->transmit(serial,
(rt_uint8_t *)buffer + offset,
tx_fifo->put_size,
RT_SERIAL_TX_BLOCKING);
//更新已經發送的數據量
offset += tx_fifo->put_size;
size -= tx_fifo->put_size;
//等待傳輸完成
rt_completion_wait(&(tx_fifo->tx_cpt), RT_WAITING_FOREVER);
}
return length;
}
關于上文的函數調用:
serial->ops->transmit()函數會調用驅動層的stm32_transmit()
stm_32transmit()又直接會調用驅動層的stm32_control()對中斷進行初始化.,從而進行發送
我們直接看stm32_control(serial, RT_DEVICE_CTRL_SET_INT, (void * )tx_flag);
//驅動層中斷發送函數
static rt_err_t stm32_control(struct rt_serial_device *serial,
int cmd, void *arg)
{
...
case RT_DEVICE_CTRL_SET_INT:
//設置單個中斷搶占優先級和響應優先級
HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0);
//設置使能中斷通道
HAL_NVIC_EnableIRQ(uart->config->irq_type);
if (ctrl_arg == RT_DEVICE_FLAG_INT_TX)
//開啟TXE中斷
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TXE);
break;
...
}
此處,我們結合中斷,來梳理中斷發送的大致流程:
說明:
TDR: TX Data Register 發送數據寄存器
TXE: TX Data Register Empty 發送數據寄存器為空
TC: Transmission Complete 發送完成
uart_isr() : 驅動層中斷服務函數
rt_hw_serial_isr() : 驅動框架層中斷服務函數
tx_fifo: 發送緩沖區
假設有1024字節的數據通過串口發送,發送緩沖區設置為128;
用戶調用rt_device_open(uart_dev,RT_SERIAL_TX_BLOCKING)與rt_device_write(uart_dev,buffer,1024);
1024字節中先有128個字節先被復制到緩沖區,調用驅動層stm32_transmit(),初始化中斷相關函數,啟動發送
中斷服務函數uart_isr()檢測到TXE中斷,將一個字節放入TDR中
(在TDR中的數據后續會被轉移至移位寄存器,TDR因此又會空出來)
中斷服務函數uart_isr()會接收后續的TXE中斷,從而將tx_fifo中的數據都被依次送往TDR寄存器
當緩沖區中沒有可發送的數據時,關閉TXE中斷,開啟TC中斷
如果檢測到TC中斷,則代表移位寄存器中所有的數據都被發送完,此時關閉TC中斷,驅動層中斷服務函數uart_isr()調用應用層中斷服務函數rt_hw_serial_isr()
在rt_hw_serial_isr()中,檢測tx_fifo是否為空,為空喚醒完成量,rt_device_write()進行新一輪的組阻塞發送
當所有1024個數據被分8次發送完成,rt_device_write()返回,否則重復3-8步
static void uart_isr(struct rt_serial_device *serial)
{
//TXE標志位被設立,且檢測到TXE中斷
if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TXE) != RESET) &&(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TXE)) != RESET)
{
...
//如果能夠從tx_fifo中讀到一個字節的數據,則將其發送到TDR中,
if (rt_ringbuffer_getchar(&(tx_fifo->rb), &put_char))
{
UART_SET_TDR(&uart->handle, put_char);
}
else
{
//如果沒有數據可以發送,則關TXE中斷,開TC中斷
__HAL_UART_DISABLE_IT(&(uart->handle), UART_IT_TXE);
__HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TC);
}
}
...
//TC標志位被設立,且檢測到TC中斷
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
{
//傳輸完成,則關閉TC中斷,調用驅動框架層中斷服務函數rt_hw_serial_isr()
__HAL_UART_DISABLE_IT(&(uart->handle), UART_IT_TC);
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_TX_DONE);
}
}
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
case RT_SERIAL_EVENT_TX_DONE:
{
//獲取tx_fifo中的數據長度
tx_length = rt_ringbuffer_data_len(&tx_fifo->rb);
if (tx_length == 0)
{
tx_fifo->activated = RT_FALSE;
//tx_fifo中的數據全部發送完,則喚醒完成量,進行下一次發送
if (serial->parent.tx_complete != RT_NULL)
serial->parent.tx_complete(&serial->parent, RT_NULL);
//設置回調函數
if (serial->parent.open_flag & RT_SERIAL_TX_BLOCKING)
rt_completion_done(&(tx_fifo->tx_cpt));
break;
}
}
}
一個可能存在的問題:
因為中斷發送應用層數據量可能會大于發送緩沖區大小,因此可能需要多次發送,例如上文發送1024個數據需要將數據分8次拷貝到緩存中,然而,在上述代碼中我們可以發現,在一次發送完,tx_fifo的激活標識就被設置為flase了,這意味者,還在工作的tx_fifo被標注為不工作,那么在多線程環境下,這很可能會導致惡性的bug,針對此問題,筆者已經提了相關的pr(#7997).
//有問題的代碼
if (tx_length == 0)
{
tx_fifo->activated = RT_FALSE;
...
}
總結,中斷發送跟輪詢發送明顯的區別在于,中斷發送是直接將數據放入RDR(RX Data Register),然后返回,而輪詢發送,有用while死等TC中斷這一過程.因此中斷發送相較于輪詢發送,減少了對于CPU的占用率.
//中斷發送
UART_SET_TDR(&uart->handle, put_char);
//輪詢發送
UART_SET_TDR(&uart->handle, c);
while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
DMA
//DMA發送 框架層函數
static rt_ssize_t _serial_fifo_tx_blocking_nbuf(
struct rt_device *dev, rt_off_t pos,
const void *buffer, rt_size_t size)
{
if (tx_fifo->activated == RT_TRUE) return 0;
//將激活標識設置為TRUE
tx_fifo->activated = RT_TRUE;
//開啟DMA發送
rst = serial->ops->transmit(serial,
(rt_uint8_t *)buffer,
size,
RT_SERIAL_TX_BLOCKING);
//等待完成量的喚醒
rt_completion_wait(&(tx_fifo->tx_cpt), RT_WAITING_FOREVER);
return rst;
}
//DMA發送 驅動層函數
static rt_ssize_t stm32_transmit(struct rt_serial_device *serial,
rt_uint8_t *buf,
rt_size_t size,
rt_uint32_t tx_flag)
{
if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_TX)
{
//調用DMA傳輸
HAL_UART_Transmit_DMA(&uart->handle, buf, size);
return size;
}
}
DMA的大致傳輸流程:
說明:
TC: Transmission Complete
假設有256字節的數據通過串口發送,發送緩沖區設置為256
用戶調用rt_device_open(uart_dev,RT_SERIAL_TX_BLOCKING)與rt_device_write(uart_dev,buffer,256);
驅動層調用HAL_UART_Transmit_DMA(),開啟DMA數據發送
數據發送完成后,驅動層中斷服務函數uart_isr()會接收TC中斷,繼而調用HAL_UART_TxCpltCallback函數中的rt_hw_serial_isr().在驅動框架層的中斷服務函數中,會喚醒完成量,結束一次DMA發送
//DMA發送 驅動層函數
static void uart_isr(struct rt_serial_device *serial)
{
...
//接收到TC中斷
if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
{
if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_TX)
{
//HAL_UART_TxCpltCallback()將被調用
HAL_UART_IRQHandler(&(uart->handle));
}
}
}
//DMA發送 驅動層函數
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
//計算發送數量
level = rt_hw_interrupt_disable();
trans_total_index = __HAL_DMA_GET_COUNTER(&(uart->dma_tx.handle));
rt_hw_interrupt_enable(level);
if (trans_total_index) return;
//調用驅動層中斷服務函數
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_TX_DMADONE);
}
//DMA發送驅動框架層函數
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
case RT_SERIAL_EVENT_TX_DMADONE:
{
//將激活標志設置為FALSE,其他線程可以使用串口發送數據
tx_fifo->activated = RT_FALSE;
//設置回調函數的實參
if (serial->parent.tx_complete != RT_NULL)
serial->parent.tx_complete(&serial->parent, RT_NULL);
if (serial->parent.open_flag & RT_SERIAL_TX_BLOCKING)
{
//喚醒完成量
rt_completion_done(&(tx_fifo->tx_cpt));
break;
}
}
}
DMA發送只需要調用HAL_UART_Transmit_DMA(),開啟DMA發送,然后等待TC中斷,發送的具體過程都由DMA自行處理,不需要像中斷一樣,還需要進行持續接收TXE中斷,將數據放入TDR寄存器,以及關閉TXE中斷開啟TC中斷等一系列操作,因此進一步降低了CPU的占用率,節省系統資源.
寫在最后
以上就是對于串口設備框架serial_v2的源碼分析,鑒于篇幅,只分析了阻塞模式,非阻塞模式的整體框架與設置模式是相同的。
-
驅動器
+關注
關注
52文章
8154瀏覽量
145996 -
緩沖器
+關注
關注
6文章
1917瀏覽量
45450 -
STM32
+關注
關注
2266文章
10871瀏覽量
354787 -
串口中斷
+關注
關注
0文章
64瀏覽量
13859 -
RT-Thread
+關注
關注
31文章
1272瀏覽量
39919
發布評論請先 登錄
相關推薦
評論