0.摘要
本文以STM32F1x系列單片機爲例,主要介紹了串口的初始化、串口中斷、接收/發送、串口調試等內容,也順帶講到中斷分組、半主機模式以及微庫MicroLIB。
1.串口初始化
串口初始化主要包括對IO、USART和中斷的初始化。根據STM32F1x手冊RM0008的P166,USART在全雙工模式下,發送口TX要配置成複用推輓輸出,接收口RX要配置成浮空輸入或上拉輸入。此外,本文不使用USART的硬件流控制,所謂硬件流控制就是通過加入額外的引腳(RTS和CTS)來控制數據的收發過程,在數據傳輸之前確認收發雙方均準備好才進行通信,用於防止接收緩衝區滿而導致的數據丟失問題。
/*****************************************************
*function: 初始化串口1
*param: 串口波特率
*return:
******************************************************/
void USART1_Init(unsigned int BaudRate)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA時鐘
/* TX - PA.9 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //複用推輓輸出
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* RX - PA.10 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //PA.10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空輸入
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = BaudRate; //波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字長8位
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStructure.USART_Parity = USART_Parity_No; //無奇偶校驗
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //無硬件數據流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收/發模式
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //開啓接收中斷
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); //開啓空閒中斷
USART_Cmd(USART1, ENABLE);
}
中斷部分的配置較爲簡單,主要涉及中斷分組的問題。在說中斷分組之前,必須先了解中斷優先級:STM32F1x的中斷優先級分爲搶佔優先級(先優先級)和響應優先級(從優先級),不同搶佔優先級的中斷可相互打斷(即相互嵌套)。搶佔優先級相同的兩個中斷不可相互打斷,先發生(中斷)者先執行,如果同時發生(中斷),則高響應優先級者先執行。這兩個優先級在STM32F1x中通過寄存器的4個位來表示,究竟用多少個位表示搶佔優先級,多少個位表示響應優先級呢?STM32F1x並沒有定死,而是通過中斷分組來讓用戶靈活分配。各中斷分組定義如下圖所示(截自UM0427的P228)。本文由於只使用了一箇中斷,因此選擇任何一個分組的任何一個優先級都無所謂。
/*****************************************************
*function: 串口1中斷配置
*param:
*return:
******************************************************/
void NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //中斷分組1:1位搶佔優先級,3位響應優先級
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //中斷通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //搶佔優先級
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子優先級
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中斷
NVIC_Init(&NVIC_InitStructure);
}
2.串口接收
串口接收通過中斷來實現,即當接收到數據時,產生中斷,程序轉去處理接收到的數據。接收數據用的中斷包括接收中斷(RXNE)和空閒中斷(IDLE),如下圖所示(截自RM0008的P821)。接收中斷較好理解,就是每接收到一個字節的數據,就會產生中斷。而空閒中斷則是在接收完多個連續的字節(即一個數據幀)之後產生中斷。
本文同時開啓接收中斷和空閒中斷,串口每收到一個字節的數據,就進入接收中斷,把它讀取出來放好。當收完一幀數據時,就會進入空閒中斷,把所接收的n個字節的數據打印出來(串口打印見下一小節)。由於同個USART的中斷共用一箇中斷服務函數,故在函數中需要對中斷源進行判斷,再執行相應的操作。值得注意的是,每次進入中斷都要讀一下DR寄存器(Data Register),否則將不斷地進入中斷。
/*****************************************************
*function: 串口1中斷服務函數,打印接收到的字節
*param:
*return:
******************************************************/
void USART1_IRQHandler(void)
{
static unsigned char buff[64];
static unsigned char n = 0;
unsigned char i;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //判斷是否爲接收中斷
{
buff[n++] = USART1->DR; //讀取接收到的字節數據
if(n == 64)
{
n = 0;
}
}
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //判斷是否爲空閒中斷
{
USART1->DR; //讀DR,清標誌
printf("%d characters:\r\n", n);
for(i=0; i<n; i++)
{
printf("buff[%d] = 0x%02hhx\r\n", i, buff[i]); //輸出十六進制,保留最低兩位,不夠補0
}
n = 0;
}
}
3.串口發送與printf打印
前面說的的DR寄存器是一個可讀寫寄存器(實際上是由兩個寄存器組成),串口的發送和接收都要圍着它轉,收到的數據從它裏面讀,而發送的數據要往它裏面扔。串口的發送操作非常簡單,一條語句就能搞定,就是往DR寄存器寫入要發送的數據:
USART1->DR = data;
或者使用庫函數:
USART_SendData(USART1, data);
串口在嵌入式領域不僅是一個通訊接口,還是一種調試工具,其好用程度不亞於硬件仿真。學過C語言的朋友應該都知道標準庫函數printf()和scanf(),前者用於打印信息到控制檯上,後者實現從鍵盤讀入字符到程序。Keil、IAR等集成開發環境均支持標準庫函數,如果在單片機的程序裏調用printf()打印內容,最終會在哪裏顯示呢?答案是不可知的,因爲單片機沒有控制檯這種東西,但我們可以利用它的外設來實現printf(),比如LCD或串口(串口再接到電腦上顯示打印信息)。串口基本上大多數單片機都有,而LCD就不一定了,所以我們通常用串口來打印內容。
那麼只要是有串口的單片機,調用一下printf()就可以打印信息了嗎?還沒那麼簡單,單片機並不能猜透你的意圖,你需要告訴它往哪裏printf,通過下面的fputc()函數來實現。fputc()是printf()的底層函數,需要把它改裝一番,讓它把要打印的數據發送到串口上去。
/*****************************************************
*function: 寫字符文件函數
*param1: 輸出的字符
*param2: 文件指針
*return: 輸出字符的ASCII碼
******************************************************/
int fputc(int ch, FILE *f)
{
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); //等待上次發送結束
USART_SendData(USART1, (unsigned char)ch); //發送數據到串口
return ch;
}
除此之外,我們還要再做一點配置工作——禁用半主機模式,禁用了半主機模式才能使用標準庫函數printf()打印信息到串口,在程序中加入以下代碼即可。那麼什麼是半主機模式?爲什麼不用它?半主機模式是ARM單片機的一種調試機制,跟串口調試不一樣的是,它需要通過仿真器來連接電腦和ARM單片機,並調用相應的指令來實現單片機向電腦顯示器打印信息(或者從電腦鍵盤讀取輸入)。簡而言之,這種方法比串口調試更復雜(需要進行更多的配置操作),也更不靈活(一定要用仿真器)。
/********** 禁用半主機模式 **********/
#pragma import(__use_no_semihosting)
struct __FILE
{
int a;
};
FILE __stdout;
void _sys_exit(int x)
{
}
上面的配置似乎有點麻煩,要加入這麼一堆難懂的代碼,難道沒有更簡便點的方法嗎?有,但不推薦。方法是使用微庫(MicroLIB),只要在Keil的“Options for Target -> Target ->Use MicroLIB”上打鉤,即可使用串口打印(fputc()函數還是要改,但上述代碼不用加)。微庫是區別於C標準庫的另一個庫,當使用微庫時,就默認關閉了半主機模式,也就不用添加上面的代碼。這樣雖然方便,但個人建議能不用就不用,原因:第一,微庫是爲小內存嵌入式設備而設計的,使用它可以減少代碼所佔空間,但對現在STM32等單片機來說,內存一般都夠用,微庫並非必需;第二,微庫相對於C標準庫而言,支持的功能更少,主要體現在對操作系統的支持上。總的來說,標準的東西總是相對更可靠,所以不必要的掉坑,還是用C標準庫,不用微庫。
4.最後
我們還需要一個USB轉TTL模塊和一臺裝有串口調試軟件的電腦,就可以看到單片機打印到串口上的內容了。從此,如果我們想看某個變量的值,可以打印一下,想看程序跑到哪個地方,也可以打印一下,想讓單片機向世界say個hello,還是可以打印一下。媽媽再也不用擔心我的調試!
主函數:
int main()
{
USART1_Init(115200);
NVIC_Config();
printf("Hello, world!\r\n");
printf("Please enter any character:\r\n");
while(1);
}
運行效果:
參考:
[2] UM0427 (STM32F101x/STM32F103x固件庫手冊)
[3] Keil官方對半主機模式semihosting的介紹