有關USART的DMA傳輸模式,其基本的概念和配置,網上有很多博客和教程都有,這里不再贅述,只是記錄一下比較容易忽視而造成調試不通的問題。
1. 串口發送和接收分屬兩個DMA通道
一般方式操作串口時,讀寫數據都是只操作DR(數據寄存器),雖然它是由兩個寄存器組成的,一個給發送用(TDR),一個給接收用(RDR),但是用戶只能操作DR寄存。而DMA模式下,串口發送和接收分屬兩個DMA通道,需要單獨配置。
分別配置的代碼如下:
static void USART1_Tx_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn; // 配置DMA1_Channel4中斷
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_DeInit(USART_TX_DMA_CHANNEL);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 開啟DMA時鍾
DMA_InitStructure.DMA_PeripheralBaseAddr = USART_DR_ADDRESS; // 設置DMA源地址:串口數據寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)sendbuff; // 內存地址(要傳輸的變量的指針)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:從內存到外設
DMA_InitStructure.DMA_BufferSize = CMD_NUM; // 傳輸大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外設地址不增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 內存地址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外設數據單位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 內存數據單位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // DMA一次模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 優先級:中
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止內存到內存的傳輸
DMA_Init(USART_TX_DMA_CHANNEL, &DMA_InitStructure); // 配置DMA通道DMA1_Channel4
DMA_ITConfig(USART_TX_DMA_CHANNEL,DMA_IT_TC,ENABLE);
DMA_Cmd (USART_TX_DMA_CHANNEL,DISABLE); // 關閉DMA
}
static void USART1_Rx_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
//注意,接收沒使用接收DMA中斷
// NVIC_InitTypeDef NVIC_InitStructure;
//
// NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn;
// NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
// NVIC_InitStructure.NVIC_IRQChannelSubPriority = 4;
// NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// NVIC_Init(&NVIC_InitStructure);
DMA_DeInit(USART_RX_DMA_CHANNEL);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 開啟DMA時鍾
DMA_InitStructure.DMA_PeripheralBaseAddr = USART_DR_ADDRESS; // 設置DMA源地址:串口數據寄存器地址*/
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_cmd; // 內存地址(要傳輸的變量的指針)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 方向:外設到內存
DMA_InitStructure.DMA_BufferSize = CMD_NUM; // 傳輸大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外設地址不增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 內存地址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外設數據單位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 內存數據單位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // DMA一次模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 優先級:中
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止內存到內存的傳輸
DMA_Init(USART_RX_DMA_CHANNEL, &DMA_InitStructure); // 配置DMA通道DMA1_Channel5
// DMA_ITConfig(USART_RX_DMA_CHANNEL,DMA_IT_TC,ENABLE);
DMA_Cmd (USART_RX_DMA_CHANNEL,ENABLE); // 使能DMA
}
注意:在串口的基本配置當中要打開DMA傳輸模式,函數如下:
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); // 開啟串口發送DMA
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); // 開啟串口接收DMA
2. 間隔單次傳輸
將DMA傳輸模式設置為Normal(一次傳輸),傳輸完成需要再次傳輸時,需要再次向DMA通道的傳輸數量寄存器(CNDTR)寫入要傳輸的字節數。但是,在寫入前,需要關閉DMA,寫完CNDTR后再打開。
2.1 串口DMA發送
我的設計方法是在初始化的時候,默認先關閉發送DMA,在需要串口發送數據時,先配置CNDTR,再打開DMA,發送完成后進入中斷函數,再關閉DMA。
void DMA1_Channel4_IRQHandler(void)
{
DMA_ClearFlag(DMA1_FLAG_TC4);
DMA_Cmd(USART_TX_DMA_CHANNEL,DISABLE);
}
*********
//代碼片段
DMA_SetCurrDataCounter(DMA1_Channel4,(uint16_t)CMD_NUM); // 關於DMA單次傳輸,這條非常重要
DMA_Cmd (USART_TX_DMA_CHANNEL,ENABLE);
2.2 串口DMA接收
設計方法是:不啟用DMA接收通道中斷,而使用串口傳輸中斷,在串口中斷函數中對DMA處理。注意,一般串口中斷我們采用的是接收中斷USART_IT_RXNE,接收一次即中斷一次。在DMA模式下要使用空閑中斷USART_IT_IDLE,空閑中斷是在檢測到接收數據后,在數據總線上的一個字節時間內,如果沒有接收到新的數據,則觸發空閑中斷,它是在串口的RXNE位被置位之后才開始檢測。簡單理解是,連續的一串數據發送完成之后,才觸發空閑中斷。
串口的CR1寄存器的IDLE位被硬件置1,必須采用軟件將IDLE位清零才能避免反復進入空閑中斷。具體的做法是先讀取狀態寄存器USART_SR,再讀取數據寄存器USART_DR,完成后自動清除。需要注意的是,不能采用庫函數USART_ClearFlag()
或者USART_ClearItPending()
來清除IDEL標志,因為這兩個函數接收的中斷標志位僅包括:
USART_FLAG_CTS: CTS Change flag (not available for UART4 and UART5).
USART_FLAG_LBD: LIN Break detection flag.
USART_FLAG_TC: Transmission Complete flag.
USART_FLAG_RXNE: Receive data register not empty flag.
同理,關閉DMA后,重置傳輸字節數,再開啟DMA(因為串口一直要監測接收數據)。串口中斷函數基礎代碼如下:
void USART1_IRQHandler(void)
{
uint32_t temp = 0;
if(USART_GetITStatus(USART1,USART_IT_IDLE)!=RESET)
{
// temp = USART_GetITStatus(USART1,USART_IT_IDLE); // 在判斷時已經讀取過一次
temp = USART_ReceiveData(USART1); // 必須添加這條語句
DMA_Cmd(USART_RX_DMA_CHANNEL,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5,(uint16_t)CMD_NUM);
DMA_Cmd(USART_RX_DMA_CHANNEL,ENABLE);
}
}
3. 疑問
實際上這里面還有一些隱含方式方法,感興趣的可以嘗試一下,歡迎分享。
- 現在采用的是串口中斷來處理接收問題,是否可以采用DMA接收中斷來處理數據接收?就如同DMA發送中斷來處理發送數據一樣。
4. 參考文獻
- 《STM32F10X參考手冊》
- 《32位基於ARM微控制器STM32F101xx與STM32F103xx 固件函數庫》
- STM32的串口空閑中斷
- STM32的串口采用DMA方式接收數據測試
- STM32使用串口IDLE中斷的兩種接收不定長數據的方式