寫這個文章是用來學習的,記錄一下我的學習過程。希望我能一直堅持下去,我只是一個小白,只是想好好學習,我知道這會很難,但我還是想去做!
本文寫于:2025.04.08
STM32開發板學習——第27節: [9-3] USART串口發送&串口發送+接收
- 前言
- 開發板說明
- 引用
- 解答和科普
- 一、串口發送
- 二、發送+接收
- 問題
- 總結
前言
? ?本次筆記是用來記錄我的學習過程,同時把我需要的困難和思考記下來,有助于我的學習,同時也作為一種習慣,可以督促我學習,是一個激勵自己的過程,讓我們開始32單片機的學習之路。
? ?歡迎大家給我提意見,能給我的嵌入式之旅提供方向和路線,現在作為小白,我就先學習32單片機了,就跟著B站上的江協科技開始學習了.
? ?在這里會記錄下江協科技32單片機開發板的配套視頻教程所作的實驗和學習筆記內容,因為我之前有一個開發板,我大概率會用我的板子模仿著來做.讓我們一起加油!
? ?另外為了增強我的學習效果:每次筆記把我不知道或者問題在后面提出來,再下一篇開頭作為解答!
開發板說明
? ?本人采用的是慧凈的開發板,因為這個板子是我N年前就買的板子,索性就拿來用了。另外我也購買了江科大的學習套間。
? ?原理圖如下
1、開發板原理圖
2、STM32F103C6和51對比
3、STM32F103C6核心板
視頻中的都用這個開發板來實現,如果有資源就利用起來。另外也計劃實現江協科技的套件。
下圖是實物圖
引用
【STM32入門教程-2023版 細致講解 中文字幕】
還參考了下圖中的書籍:
STM32庫開發實戰指南:基于STM32F103(第2版)
數據手冊
解答和科普
一、串口發送
這是USB轉串口模塊,這里有個跳線帽,說過這個跳線帽要接在VCC和3.3V上,因為VCC是給CH340芯片供電,選擇通信的TTL電平為3.3V, 然后通信引腳TXD和RXD要接在STM32的PA9和PA10口,為什么是這兩個口呢,看一下引腳定義表,計劃用USART1進行通信,所以選擇這兩個引腳。TX和RX交叉連接,不要接錯了。在接線圖里,接A9(TX)接的就是串口模塊的RXD(接受), 然后串口模塊的TXD(發送)要接在STM32的PA10(RX接收)。然后,兩個設備之間要把負極接在一起,進行共地,一般多個系統之間互聯,都要進行共地,這樣電平才能有高低的參考。最后這個串口和STlink都要插在電腦上,這樣STM32和串口模塊都要獨立供電,所以這里通信的電源正極就不需要接了,直接3根線就行了。
初始化流程:
第一步,開啟時鐘,把需要用的USART和GPIO的時鐘打開;
第二部,GPIO初始化,把TX配置成復用輸出,RX配置成輸入;
第三步,配置USART,直接使用一個結構體,就可以把這里所有參數都配置好了;
第四步,如果你只需要發送的功能,就直接開啟USART,初始化就結束了,如果你需要接受的功能,可能還需要配置中斷,那就在開啟USART之前,再加上ITConfig和NVIC的代碼就行了。
那初始化完成之后,如果要發送數據,調用一個發送函數就行了,如果要接受數據,就調用接受的函數,如果要獲取發送和接受的狀態,就調用獲取標志位的函數,這就是USART外設的使用思路。
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
配置同步時鐘輸出的,包括時鐘是不是要輸出,時鐘的極性相位等參數,因為參數比較多,也是用結構體配置的;
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
開啟USART到DMA的觸發通道;
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
這兩個函數,在我們發送和接收的時候會用到;寫和讀DR寄存器,DR寄存器內部有4個寄存器,控制發送與接收,執行細節上一節已經分析過了,寫DR就是發送,讀DR就是接收,至于怎么產生波形。怎么判斷輸入,軟件不管;
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
標志位相關函數
首先引腳模式:TX引腳是USART外設控制的輸出腳,所以要選用復用推挽輸出;
RX引腳是USART外設數據輸入腳,所以要選擇輸入模式,輸入模式并不分什么普通輸入、復用輸入,一根線只能有一個輸出,但可以有多個輸入,所以輸入腳外設和GPIO都可以同時用,一般RX配置是浮空輸入或者上拉輸入,因為串口波形空閑狀態時高電平,所以不使用下拉輸入,引腳模式不清楚的話,還是可以看一下手冊,GPIO那一節有個推薦的配置表,可以參考一下;目前只需要數據發送,所以只初始化TX就行了,引腳模式這里,選擇AF_PP;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);
這樣就是把PA9配置為復用推挽輸出,供USART1的TX使用,那引腳就初始化好了;
配置USART
USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600; //波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //控制流USART_InitStructure.USART_Mode=USART_Mode_Tx; //串口模式要想接收再|USART_InitStructure.USART_Parity=USART_Parity_No; //無校驗位USART_InitStructure.USART_StopBits=USART_StopBits_1; //停止位1位USART_InitStructure.USART_WordLength=USART_WordLength_8b; //數據位8位USART_Init(USART1,&USART_InitStructure);USART_Cmd(USART1,ENABLE); //開啟
第一個參數波特率:可以直接寫個9600就行,寫完之后,這個Init函數內部會自動算好9600對應的分頻系數,然后寫入到BRR寄存器;
第二個參數是硬件流控制:我們不使用所以選擇None;
第三個參數是串口模式:我們放到這里,這里可以選擇TX發送模式和RX接收模式,如果你繼續要發送有需要接收,那就用或符號把TX和RX或起來,
第四個參數是校驗位:
第五個參數是停止位;
第六個參數哦是8位數據;
發送一個字節
void Serial_SendByte(uint8_t Byte) //發送一個字節
{USART_SendData(USART1,Byte);while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);}
main.C
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Serial.h"int main(void)
{OLED_Init();Serial_Init();Serial_SendByte(0x66);OLED_ShowString(1,2,"Hello STM32 MCU");while(1){}
}
Serial
#include "stm32f10x.h" // Device headervoid Serial_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//開啟時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure); USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600; //波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //控制流USART_InitStructure.USART_Mode=USART_Mode_Tx; //串口模式要想接收再|USART_InitStructure.USART_Parity=USART_Parity_No; //無校驗位USART_InitStructure.USART_StopBits=USART_StopBits_1; //停止位1位USART_InitStructure.USART_WordLength=USART_WordLength_8b; //數據位8位USART_Init(USART1,&USART_InitStructure);USART_Cmd(USART1,ENABLE); //開啟
}void Serial_SendByte(uint8_t Byte) //發送一個字節
{USART_SendData(USART1,Byte);while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);}
#ifndef __SERIAL_H
#define __SERIAL_Hvoid Serial_Init(void);
void Serial_SendByte(uint8_t Byte);#endif
實驗現象
HEX模式:只能顯示一個個的十六進制數,不能顯示文本0x41;
文本模式;以原始數據編碼后的形式顯示 0x41對應A;
UFT8
Serial_SendByte('A');也是發送0x41;
發送數組
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{uint16_t i;for(i=0;i<Length;i++){Serial_SendByte(Array[i]);}}
發送字符串
void Serial_SendString(char *String)
{uint8_t i;for(i=0;String[i]!='\0';i++){Serial_SendByte(String[i]);}}
Serial_SendString("Hello STM32 MCU\r\n");
這個可以執行換行:\r\n,
發送數字
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{uint32_t Result=1;while (Y--){Result *=X;}return Result;}
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{uint8_t i;for (i=0;i< Length;i++){Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+'0');}}
使用printf函數
printf函數默認輸出到屏幕,我們單片機沒有屏幕,所以要進行重定向:
在串口模塊里加上#include <stdio.h>
int fputc(int ch,FILE *f)
{Serial_SendByte(ch);return ch;
}
printf("Num= %d\r\n",666);
那么重定向fput跟printf有什么關系呢:fputc是printf函數的底層,printf函數在打印的時候,就是不斷調用fput函數一個個打印的,我們把fputc函數重定向到了串口,那printf自然就輸出到串口了,這樣printf就移植好了。
這種方法只能重定向一個,你定向到串口1了,那串口2再用就沒有用了,如果多個串口都想用printf怎么辦呢,這時候就可以用Sprintf,可以把格式化字符輸出到一個字符串里,所以這里可以先定義一個字符串:
char String[100];sprintf(String,"Num= %d\r\n",666);Serial_SendString(String);
定義字符串,打印字符串;再發送字符串。我們要是能封裝這個過程,就再好不過了。
封裝Sprintf;可變參數
void Serial_Printf(char *format,...)
{char String[100];va_list arg;va_start(arg,format);vsprintf(String, format ,arg);va_end(arg);Serial_SendString(String);}
Serial_Printf("Num= %d\r\n",666);
顯示漢字
1、UTF8都是(加上–no-multibyte-chars)
Serial_Printf(“你好,世界”);
2、GB2312
然后再寫漢字,選擇GBK編碼,復位也可以顯示了。
二、發送+接收
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitStructure.USART_Mode=USART_Mode_Tx |USART_Mode_Rx;
可以選擇查詢或者中斷,如果使用查詢,那初始化就結束了,如果使用中斷,那還需要在這里開啟中斷,配置NVIC;
查詢:
查詢的流程是,在主函數里不斷判斷RXNE標志位,如果置1了,就說明收到數據了, 那再調用ReceiveData,讀取DR寄存器,這樣就行了。主函數演示不封裝了,
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Serial.h"uint8_t RxData;
int main(void)
{OLED_Init();Serial_Init();while(1){if(USART_GetFlagStatus(USART1 ,USART_FLAG_RXNE)== SET) //收到數據{RxData=USART_ReceiveData(USART1);}OLED_ShowHexNum(1,1,RxData,2);//清除標志位}
}
中斷方法
首先,初始化這里,要加上開啟中斷的代碼:
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn ;NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);
這里RXNE標志位一但置1了,就會向NVIC申請中斷,之后我們可以在中斷函數里接收數據;
要不要清楚標志位呢,如果你讀取了DR,就可以自動清除,如果沒讀取DR,就可以手動清除。
其實在中斷里,只是進行了數據轉存一下,最終還是要掃描查詢這個RxFlag,來接收數據的;對于單字節接收來說,可能轉存一下意義不大,主要展示一下中斷接收的寫法和多字節數據包接收;
在各自的函數中分別用了;
void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET){Serial_RxData=USART_ReceiveData(USART1);Serial_RxFlag=1;USART_ClearITPendingBit(USART1,USART_IT_RXNE);}}
uint8_t Serial_GetRxFlag(void)
{if(Serial_RxFlag == 1){Serial_RxFlag=0;return 1;}return 0;
}uint8_t Serial_GetRxData (void)
{return Serial_RxData;
}
main.C
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Serial.h"uint8_t RxData;
int main(void)
{OLED_Init();Serial_Init();OLED_ShowString(1,1,"Hello STM32 MCU");OLED_ShowString(2,1,"RxData:");while(1){if(Serial_GetRxFlag()==1) //收到數據{RxData=Serial_GetRxData();Serial_SendByte(RxData);}OLED_ShowHexNum(2,8,RxData,2);//清除標志位}
}
Serial.ch
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdarg.h"uint8_t Serial_RxData;
uint8_t Serial_RxFlag;void Serial_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//開啟時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure); USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600; //波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //控制流USART_InitStructure.USART_Mode=USART_Mode_Tx |USART_Mode_Rx; //串口模式要想接收再|USART_InitStructure.USART_Parity=USART_Parity_No; //無校驗位USART_InitStructure.USART_StopBits=USART_StopBits_1; //停止位1位USART_InitStructure.USART_WordLength=USART_WordLength_8b; //數據位8位USART_Init(USART1,&USART_InitStructure);USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn ;NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);USART_Cmd(USART1,ENABLE); //開啟
}void Serial_SendByte(uint8_t Byte) //發送一個字節
{USART_SendData(USART1,Byte);while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);}void Serial_SendArray(uint8_t *Array,uint16_t Length)
{uint16_t i;for(i=0;i<Length;i++){Serial_SendByte(Array[i]);}}void Serial_SendString(char *String)
{uint8_t i;for(i=0;String[i]!='\0';i++){Serial_SendByte(String[i]);}}
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{uint32_t Result=1;while (Y--){Result *=X;}return Result;}
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{uint8_t i;for (i=0;i< Length;i++){Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+'0');}}int fputc(int ch,FILE *f)
{Serial_SendByte(ch);return ch;
}void Serial_Printf(char *format,...)
{char String[100];va_list arg;va_start(arg,format);vsprintf(String, format ,arg);va_end(arg);Serial_SendString(String);}uint8_t Serial_GetRxFlag(void)
{if(Serial_RxFlag == 1){Serial_RxFlag=0;return 1;}return 0;
}uint8_t Serial_GetRxData (void)
{return Serial_RxData;
}void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET){Serial_RxData=USART_ReceiveData(USART1);Serial_RxFlag=1;USART_ClearITPendingBit(USART1,USART_IT_RXNE);}}
現象
問題
1、只是發送和接收一個字節,如何接收大量數據;
總結
本節課主要學會了如何用串口進行接收和發送,在串口模式可以|起了,發送和接收,然后本次只完成了單字節的接收和發送。