天天看點

STM32的UART讀寫及printf列印

0.摘要

本文以STM32F1x系列單片機為例,主要介紹了序列槽的初始化、序列槽中斷、接收/發送、序列槽調試等内容,也順帶講到中斷分組、半主機模式以及微庫MicroLIB。

1.序列槽初始化

序列槽初始化主要包括對IO、USART和中斷的初始化。根據STM32F1x手冊RM0008的P166,USART在全雙工模式下,發送口TX要配置成複用推挽輸出,接收口RX要配置成浮空輸入或上拉輸入。此外,本文不使用USART的硬體流控制,所謂硬體流控制就是通過加入額外的引腳(RTS和CTS)來控制資料的收發過程,在資料傳輸之前确認收發雙方均準備好才進行通信,用于防止接收緩沖區滿而導緻的資料丢失問題。

STM32的UART讀寫及printf列印
/*****************************************************
*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)。本文由于隻使用了一個中斷,是以選擇任何一個分組的任何一個優先級都無所謂。

STM32的UART讀寫及printf列印
/*****************************************************
*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)。接收中斷較好了解,就是每接收到一個位元組的資料,就會産生中斷。而空閑中斷則是在接收完多個連續的位元組(即一個資料幀)之後産生中斷。

STM32的UART讀寫及printf列印

本文同時開啟接收中斷和空閑中斷,序列槽每收到一個位元組的資料,就進入接收中斷,把它讀取出來放好。當收完一幀資料時,就會進入空閑中斷,把所接收的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);
}
           

運作效果:

STM32的UART讀寫及printf列印

參考:

[1] RM0008 (STM32F1x使用者手冊)

[2] UM0427 (STM32F101x/STM32F103x固件庫手冊)

[3] Keil官方對半主機模式semihosting的介紹

[4] Keil官方對微庫MicroLib的介紹