本节我们将会对STM32F103
的硬件资源GPIO
和串口进行介绍。
一、GPIO
1.1 电路原理图
LED
电路原理图如下图所示:
其中:
LED1
连接到PA8
引脚,低电平点亮;LED2
连接到PD2
引脚,低电平点亮;
1.2 GPIO
引脚介绍
STM32F103
系列共有7组GPIO
,分别为 GPIOA
, GPIOB
, GPIOC
, GPIOD
、 GPIOE
、GPIOF
、GPIOG
。其中,GPIOA
、GPIOB
、GPIOC
、GPIOD
是常用的,GPIOE
~GPIOG
只有一部分引脚在某些型号的STM32F103
系列中有效。
每组GPIO
通常包含16
个引脚。但并非所有引脚都在所有STM32F103
型号中都有效,具体取决于具体芯片封装。
GPIOA
:16 个引脚(PA0 - PA15
);GPIOB
:16 个引脚(PB0 - PB15
);GPIOC
:16 个引脚(PC0 - PC15
);GPIOD
:16 个引脚(PD0 - PD15
);GPIOE
:16个引脚(PE0 - PE15
);GPIOF
:16个引脚(PF0 - PF15
);GPIOG
:16个引脚(PG0 - PG15
)。
1.3 GPIO
寄存器
在STM32F103
中,GPIO
引脚的配置和控制是通过寄存器来实现的。每个GPIO
端口(如GPIOA
、GPIOB
等)都有一组寄存器来控制引脚的方向、输出类型、上拉/下拉电阻、输入模式等。;
- 两个32位配置寄存器,
GPIOx_CRH
、GPIOx_CRL
; - 两个32位数据寄存器,
GPIOx_IDR
、GPIOx_ODR
; - 一个32位设置/清除寄存器,
GPIOx_BSRR
; - 一个16位复位寄存器,
GPIOx_BRR
; - 一个32位锁定寄存器,
GPIOx_LCKR
;
1.3.1 端口配置低寄存器(GPIOx_CRL
)
GPIOx_CRL
端口配置低8位寄存器(x=A~E
),四位控制1个引脚,共控制8个引脚(0~7引脚);
31:28 | 27:24 | 23:20 | 19:16 | 15:12 | 11:8 | 7:4 | 3:0 |
---|---|---|---|---|---|---|---|
CNF7[1:0] MODE7[1:0] | ... | .... | .... | .... | .... | ..... | CNF0[1:0] MODE0[1:0] |
其中:
MODE[1:0] | CNF[1:0] | |
---|---|---|
输入模式 | 0 0 | 0 0:模拟输入模式 0 1:浮空输入模式 1 0:上拉/下卡输入模式 1 1:保留 |
输出模式 | 0 1:最大速度10MHz 1 0:最大速度2MHz 1 1:最大速度50MHz | 0 0:通用推挽输出模式 0 1:通用开漏输出模式 1 0:复用功能推挽输出模式 1 1:复用功能开漏输出模式 |
1.3.2 端口配置高寄存器(GPIOx_CRH
)
GPIOx_CRH
四位控制1个引脚(x=A~E
),共控制8个引脚(8~15引脚);同上不再重复介绍。
1.3.3 端口输入数据寄存器(GPIOx_IDR
)
GPIOx_IDR
端口输入数据寄存器(x=A~E
);
31~16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
保留 | IDR15 | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | IDR0 |
31~16
位始终读0,低16位为只读,并且只能以字(16位)的形式读出。
1.3.4 端口输出数据寄存器(GPIOx_ODR
)
GPIOx_ODR
端口输出数据寄存器(x=A~E
);
31~16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
保留 | ODR15 | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | ODR0 |
31~16
位始终读0,低16位可读可写,并且只能以字(16位)的形式操作。
1.3.5 端口设置/清除寄存器(GPIOx_BSRR
)
GPIOx_BSRR
端口设置/清除寄存器(x=A~E
);
31~16 | 15~0 |
---|---|
BR15~BR0 | BS15~BS0 |
清除端口x的位y,这些位只能以字(16位)的形式操作。 0:对对应的ODRy位不产生影响 1:清除对应的ODRy位为0 | 设置端口x的位y,这些位只能以字(16位)的形式操作。 0:对对应的ODRy位不产生影响 1:设置对应的ODRy位为1 |
1.3.6 端口清除寄存器(GPIOx_BRR
)
GPIOx_BRR
端口清除寄存器(x=A~E
);
31~16 | 15~0 |
---|---|
保留 | BR15~BR0 |
清除端口x的位y,这些位只能以字(16位)的形式操作。 0:对对应的ODRy位不产生影响 1:清除对应的ODRy位为1 |
1.4 GPIO
输出输出编码流程
GPIO
的输入/输出一般步骤如下:
(1)使能外设时钟:RCC->APB2ENR
;
在设置STM32
外设的时候,任何时候都需要先使能该外设的始终,APB2ENR
是APB2
(高速外设)总线上的外设时钟使能寄存器;
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
---|---|---|---|---|---|---|---|
ADC3 EN | UART1 EN | TIM8 EN | SPI1 EN | TIM1 EN | ADC2 EN | ADC1 EN | IOPG |
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
IOPF | IOPE | IOPD | IOPC | IOPB | IOPA | 保留 | AFIO EN |
其中对应位置1表示使能。如果使能I/O复用,需要设置AFIO EN
位为1。
(2)配置输入/输出:GPIOx->CRL
、GPIOx->CRH
;
(3)输入GPIOx->IDR
、输出GPIOx->ODR
。
1.5 代码实现
1.5.1 GPIO
初始化函数
/******************************************************** ********************
* File Name : gpio.c
* Author : Zhengyang
* Description : 初始化GPIO口
portx:端口号PORTA~E
pinx:引脚号PORT0~15
gpio_cfg:输入输出模式配置
bit:输出电平设置 高:HIGH 低:LOW
********************************************************************************/
void gpio_init(PORTx_PINx portx_pinx,GPIO_CFG gpio_cfg,BIT_ACTION bit)
{
GPIO_TypeDef *GPIO;
u8 portx = portx_pinx/16; //获取端口号
u8 pinx = portx_pinx%16; //获取引脚号
if(portx==0)
GPIO=GPIOA;
else if(portx==1)
GPIO=GPIOB;
else if(portx==2)
GPIO=GPIOC;
else if(portx==3)
GPIO=GPIOD;
else
GPIO=GPIOE;
RCC->APB2ENR|=1<<(portx+2); //外设时钟使能
if(pinx<8)
{
GPIO->CRL &=~(0x0F<<(pinx*4)); //该引脚模式配置四位清零
GPIO->CRL|=gpio_cfg<<(4*pinx); //端口配置寄存器CRL
}
else if(pinx>=8)
{
GPIO->CRH &=~(0x0F<<((pinx-8)*4)); //该引脚模式配置四位清零
GPIO->CRH|=gpio_cfg<<(4*(pinx-8)); //端口配置寄存器CRH
}
if((gpio_cfg&0x03)!=0x00) //输出
{
if(pinx<8)
GPIO->ODR|=bit<<pinx; //端口数据输出寄存器ODR
else
GPIO->ODR|=bit<<pinx; //端口数据输出寄存器
}
}
这里PORTx_PINx
、GPIO_CFG
、BIT_ACTION
均是枚举类型;
/************************************* 端口配置 ************************************************************/
typedef enum //宏定义端口配置
{
//这里的值不能改!!!
GPI = 0x00, //定义管脚输入方向
GPO_SpeedMax_10 = 0x01, //定义管脚输出方向 最大速度10MHZ
GPO_SpeedMax_2 = 0x02, //定义管脚输出方向 最大速度2MHZ
GPO_SpeedMax_50 = 0x03, //定义管脚输出方向 最大速度50MHZ
//输入模式可用
GPI_DOWN = 0x08|GPI, //输入下拉 PxODR需配置为0
GPI_UP = 0x08|GPI, //输入上拉 PxODR需配置为1 复用功能时一般采用
GPI_ANALOG = 0x00|GPI, //模拟输入
GPI_FLOAT = 0x04|GPI , //浮空输入
//输出模式不能用
GPO_PUSH_PULL = 0x00, //通用推挽输出
GPO_OPEN_DRAIN = 0x04, //通用开漏输出
GPO_MULPUSH_PULL = 0x08, //复用推挽输出
GPO_MULOPEN_DRAIN = 0x0C, //复用开漏输出
//输出模式可用
GPO_PUSH_PULL_50 = GPO_SpeedMax_50| GPO_PUSH_PULL, //通用推挽输出,最大速度50MHZ 通用输出一般设置
GPO_PUSH_PULL_10 = GPO_SpeedMax_10| GPO_PUSH_PULL, //通用推挽输出,最大速度10MHZ
GPO_PUSH_PULL_2 = GPO_SpeedMax_2 | GPO_PUSH_PULL, //通用推挽输出,最大速度2MHZ
GPO_MULPUSH_PULL_2 = GPO_SpeedMax_2| GPO_MULPUSH_PULL, //复用推挽输出,最大速度2MHZ
GPO_MULPUSH_PULL_10 = GPO_SpeedMax_10| GPO_MULPUSH_PULL, //复用推挽输出,最大速度10MHZ
GPO_MULPUSH_PULL_50 = GPO_SpeedMax_50| GPO_MULPUSH_PULL //复用推挽输出,最大速度50MHZ 复用功能 输出模式一般采用这样设置
}GPIO_CFG;
/***************************************** 宏定义引脚号 *************************************************************/
typedef enum
{
PA0=0,
PA1=1,
PA2=2,
PA3=3,
PA4=4,
PA5=5,
......
PE13=77,
PE14=78,
PE15=79
} PORTx_PINx;
typedef enum //外部端口输出电平
{
LOW = 0,
HIGH = 1
} BIT_ACTION;
1.5.2 PXout/PXIn
定义GPIO
输入输出宏:
/********************** 位运算符优先级低于算术运算符 << & | ~ 所以位运算符必须加括号 ***********************/
#define Bit_Band(Addr,Bit_Num) *((volatile unsigned long *)((Addr&0xF0000000)+0x02000000+((Addr&0xfffff)<<5)+(Bit_Num<<2)))
/********************************************** I/O 地址映射 **************************************************/
#define GPIOA_IDR (GPIOA_BASE + 0x08) //0x40010808
#define GPIOA_ODR (GPIOA_BASE + 0x0C) //0x4001080C
#define GPIOB_IDR (GPIOB_BASE + 0x08) //0x40010C08
#define GPIOB_ODR (GPIOB_BASE + 0x0C) //0x40010C0C
#define GPIOC_IDR (GPIOC_BASE + 0x08) //0x40011008
#define GPIOC_ODR (GPIOC_BASE + 0x0C) //0x4001100C
#define GPIOD_IDR (GPIOD_BASE + 0x08) //0x40011408
#define GPIOD_ODR (GPIOD_BASE + 0x0C) //0x4001180C
#define GPIOE_IDR (GPIOE_BASE + 0x08) //0x40011808
#define GPIOE_ODR (GPIOE_BASE + 0x0C) //0x4001180C
/********************************************** 宏定义输入输出 ************************************************/
#define PAout(Bit_Num) Bit_Band(GPIOA_ODR,Bit_Num) //输出 1位输出
#define PAin(Bit_Num) Bit_Band(GPIOA_IDR,Bit_Num) //输入
#define PBout(Bit_Num) Bit_Band(GPIOB_ODR,Bit_Num)
#define PBin(Bit_Num) Bit_Band(GPIOB_IDR,Bit_Num)
#define PCout(Bit_Num) Bit_Band(GPIOC_ODR,Bit_Num)
#define PCin(Bit_Num) Bit_Band(GPIOC_IDR,Bit_Num)
#define PDout(Bit_Num) Bit_Band(GPIOD_ODR,Bit_Num)
#define PDin(Bit_Num) Bit_Band(GPIOD_IDR,Bit_Num)
#define PEout(Bit_Num) Bit_Band(GPIOE_ODR,Bit_Num)
#define PEout(Bit_Num) Bit_Band(GPIOE_ODR,Bit_Num)
这里利用到了STM32
的位段功能,Cortex-M3
储存器包含两个位段(bit-band
)区,将别名寄存器的每个字映射到位段寄存器的一个位。
映射公式(外设寄存器和SRAM
可被映射):
其中:
bit_word_addr
:别名寄存器中的地址映射到某一个目标位;bit_band_base
:别名区的起始地址;byte_offset
:目标位的字节在位段的序号;bit_number
:目标位所在位置(0~31);
例如:SRAM
中某一变量地址为0x20000300
字节中的位2;
1.5.3 点亮LED
如需点亮LED
,我们只需要在main
函数添加如下代码:
gpio_init(PD2,GPO_SpeedMax_50,HIGH); // PD2接入LED2
gpio_init(PA8,GPO_SpeedMax_50,HIGH); // PA8接入LED1
while(1)
{
PAout(8) = 0;
PDout(2) = 1;
delay_ms(1500);
PAout(8) = 1;
PDout(2) = 0;
delay_ms(1500);
}
1.5.4 烧录测试
编译完成后通过ISP
烧录到开发版,我们可以看到LED1
、LED2
交替点亮,时间间隔为1.5s
。
二、串口
STM32F103
微控制器提供了5组串口,分别是USART1
, USART2
, 和USART3
、UART4
、UART5
;
- 其中
1-3
是通用同步/异步串行接口USART(Universal Synchronous/Asynchronous Receiver/Transmitter)
; 4,、5
是通用异步串行接口UART(Universal Asynchronous Receiver/Transmitter)
。
这些串口可以用来进行UART
通信,例如发送和接收数据。
2.1 串口引脚介绍
串口 | TX | RX |
---|---|---|
USART_1 | PA9 | PA10 |
USART_2 | PA2 | PA3 |
USART_3 | PB10 | PB11 |
UART_4 | PC10 | PC11 |
UART_5 | PC12 | PD2 |
2.2 串口寄存器
2.2.1 波特率设置寄存器(UARTx_BRR
)
波特率计算公式如下:$$波特率=\frac{f_{pclkx}}{ 16 \times USARTDIV}$$
其中:
PCLK1
用于USART2
、USART3
、USART4
、USART5
,最大36MHz
;PCLK2
用于USART1
,最大72MHz
。
例如:PCLK2=72MHz
,设置波特率为9600
;
USARTDIV = 72000000 / (9600 * 16) = 468.75
;
DIV_Fraction = 16 * 0.15 = 12 = 0x0C
;
DIV_Mantissa = 468.75 = 0x1D4
。
2.2.2 控制寄存器1(UARTx_CR1
)
2.2.3 控制寄存器2(UARTx_CR2
)
2.2.4 控制寄存器3(UARTx_CR3
)
2.2.5 数据寄存器(UARTx_DR
)
USART_DR
实际是包含了两个寄存器,一个专门用于发送的TDR
,一个专门用于接收的RDR
;
- 进行发送数据操作时,往
USART_DR
写入数据会自动存储在TDR
内; - 当进行读取数据操作时,向
USART_DR
读取数据会自动提取RDR
数据。
串行通信时一位一位传输的,所以TDR
和RDR
寄存器都是介于系统总线和移位寄存器间的;发送数据时把TDR
内容转移到发送移位寄存器上,接收数据时则是把接收到的每一位顺序保存在接收移位寄存器内进而转移到RDR
。
2.2.6 状态寄存器(UARTx_SR
)
状态寄存器适用于检测串口此时所处的状态。
它能够检测到的状态有:发送寄存器空位、发送完成位、读数据寄存器非空位、检测到主线空闲位、过载错误为等等。
主要关注两个位:RXNE
和TC
(第5、6两位);
RXNE
(读数据寄存器非空):当该位被置1的时候,就是提示已经有数据被接收到了,并且可以读出来了(即RDR
移位寄存器中的数据被转移到USART_DR
寄存器中);这时候要做的就是尽快读取USART_DR
,从而将该位清零,也可以向该位写0
,直接清除;TC
(发送完成):当该位被置1的时候,表示USART_DR
内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:读USART_SR
,写USART_DR
;直接向该位写0
。
2.3 串口发送/接收编码流程
(1)串口时钟使能;RCC->APB2ENR
;
在设置STM32
外设的时候,任何时候都需要先使能该外设的始终,APB2ENR
是APB2
(高速外设)总线上的外设时钟使能寄存器;
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
---|---|---|---|---|---|---|---|
ADC3 EN | UART1 EN | TIM8 EN | SPI1 EN | TIM1 EN | ADC2 EN | ADC1 EN | IOPG |
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
IOPF | IOPE | IOPD | IOPC | IOPB | IOPA | 保留 | AFIO EN |
USART1
是挂在APB2
,如果使用串口1,需要使能位14
。
(2)串口复位/停止复位;RCC->APB2RSTR
;
一般在系统刚开始配置外设时,都会先复位该外设的操作;
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
---|---|---|---|---|---|---|---|
ADC3 RST | UART1 RST | TIM8 RST | SPI1 RST | TIM1 RST | ADC2 RST | ADC1 RST | IOPG RST |
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
IOPF RST | IOPE RST | IOPD RST | IOPC RST | IOPB RST | IOPA RST | 保留 | AFIO RST |
写1复位,使其重新工作,写0结束复位。
(3)使能串口时钟,以及I/O
状态设置;
RX
:下拉输入;TX
:复用推挽输出模式;
(4)设置USART_CR1
的PEIE
和RXNEIE
;
PEIE
:校验中断使能 ,校验错误,硬件PE
位置1;RXNEIE
:使能接收中断;
(5)串口参数设置;
- 计算波特率,利用
USART_BRR
寄存器选择要求的波特率; - 设置
USART_CR1
的M
位来定义字长; - 设置
USART_CR2
中STOP
位来定义停止位的位数;
(6)设置USART_CR1
中的TE
位,发送使能;
(7)设置USART_CR1
中的RE
位,接收使能;
(8)设置USART_CR1
中的UE
位来使能串口;
(9)把要发送的数据写进USART_DR
寄存器(此动作清除TXE
)位,在只有一个缓存器的情况下对每个待发送的数据重复操作;
(10)在USART_DR
寄存器中写入最后一个数据字后,要等待UART_SR
寄存器TC=1
,它表示最后一个数据帧的传输结束,当需要关闭USART
或需要进入停机模式前。需要确认传输结束,避免破坏最后一次传输。
2.4 代码实现
2.4.1 串口初始化
/***************************************************************************************************************
* Description: 初始化串口,设置波特率
* parameter : USARTn_e 模块号(USART_1~UART_5)
baud 波特率,如9600、19200、56000、115200等
pclk pclk2时钟用于USART1 等于系统时钟=外部晶振*9 单位MHX 一般为72
pclk1时钟用于USART2~5 等于系统时钟=外部晶振*9 单位MHX 一般为36
* Sample usage: usart_init (USART_3, 9600); //初始化串口3,波特率为9600
* USARTx->BRR(32) 波特率设置寄存器 15~4:USART分频器除法因子(USARTDIV)的整数部分
3~0 :USART分频器除法因子(USARTDIV)的小数部分
波特率=f(plckx)/(16*USARTDIV)
**************************************************************************************************************/
void usart_init (USARTn usartn, u32 baud)
{
u8 pclk;
float temp;
u16 mantissa; //USARTDIV的整数部分
u16 fraction; //USARTDIV小数部分
if(usartn<1) //串口时钟选择
pclk=PCLK2;
else
pclk=PCLK1;
temp = (float)(pclk*1000000)/(baud*16);
mantissa= temp; //得到整数部分
fraction=(temp-mantissa)*16; //得到小数部分
mantissa<<=4;
mantissa+=fraction; //USART分频器除法因子(USARTDIV)
/* 配置 UART功能的 复用管脚 */
switch(usartn)
{
//***********************************************************USART1***********************************************
case USART_1:
RCC->APB2ENR |= 1<<14; //使能USART1时钟
RCC->APB2RSTR |= 1<<14; //复位USART1
RCC->APB2RSTR &=~(1<<14); //停止复位
if(USART1_TX == PA9)
{
gpio_init(PA9,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
}
else if(USART1_TX == PB6)
{
RCC->APB2ENR |= 1<<0; //使能I/O复用时钟
gpio_init(PB6,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
AFIO->MAPR |= 1<<2; //USART1重映射
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
if(USART1_RX == PA10)
{
gpio_init(PA10, GPI_DOWN,HIGH); //输入下拉
PAout(10)=0; //PxODR需配置为0
}
else if(USART1_RX == PB7)
{
gpio_init(PB7,GPI_DOWN,HIGH); //输入下拉
PBout(7)=0;
AFIO->MAPR |= 1<<2; //USART1重映射
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
#if EN_USART1_RX
USARTx[usartn]->CR1|=1<<8; //位8: PEIE 校验中断使能 校验错误,硬件PE位置1
USARTx[usartn]->CR1|=1<<5; //位5: RXNEIE 使能接收中断
#endif
#if EN_USART1_TX
USARTx[usartn]->CR1|=1<<6; //位6:TCIE 发送完成中断使能位 当SR中的TC硬件置1,将产生中断
#endif
break;
//*****************************************************USART2********************************************************
case USART_2:
RCC->APB1ENR |= 1<<17; //使能USART2时钟
RCC->APB1RSTR |= 1<<17; //复位USART2
RCC->APB1RSTR &=~(1<<17); //停止复位
if(USART2_TX == PA2)
{
gpio_init(PA2,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
}
else if(USART2_TX == PD5)
{
RCC->APB2ENR |= 1<<0; //使能I/O复用时钟
gpio_init(PD5,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
AFIO->MAPR |= 1<<3; //USART2重映射
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
if(USART2_RX == PA3)
{
gpio_init(PA3, GPI_DOWN,HIGH); //输入下拉
PAout(3)=0;
}
else if(USART2_RX == PD6)
{
gpio_init(PD6,GPI_DOWN,HIGH); //输入下拉
PDout(6)=0;
AFIO->MAPR |= 1<<3; //USART2重映射
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
#if EN_USART2_RX
USARTx[usartn]->CR1|=1<<8; //位8: PEIE 校验中断使能 校验错误,硬件PE位置1
USARTx[usartn]->CR1|=1<<5; //位5: RXNEIE 使能接收中断
#endif
#if EN_USART2_TX
USARTx[usartn]->CR1|=1<<6; //位6:TCIE 发送完成中断使能位 当SR中的TC硬件置1,将产生中断
#endif
break;
//*****************************************************USART3********************************************************
case USART_3:
RCC->APB1ENR |= 1<<18; //使能USART3时钟
RCC->APB1RSTR |= 1<<18; //复位USART3
RCC->APB1RSTR &=~(1<<18); //停止复位
if(USART3_TX == PB10)
{
gpio_init(PB10,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
}
else if(USART3_TX == PC10)
{
RCC->APB2ENR |= 1<<0; //使能I/O复用时钟
gpio_init(PC10,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
AFIO->MAPR &=~(0x03<<4); //位5~4清零
AFIO->MAPR |= 0x01<<4; //USART3部分重映射
}
else if(USART3_TX == PD8)
{
RCC->APB2ENR |= 1<<0; //使能I/O复用时钟
gpio_init(PD8,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
AFIO->MAPR &=~(0x03<<4); //位5~4清零
AFIO->MAPR |= 0x03<<4; //USART3完全重映射
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
if(USART3_RX == PB11)
{
gpio_init(PB11, GPI_DOWN,HIGH); //输入下拉
PBout(11)=0;
}
else if(USART3_RX == PC11)
{
gpio_init(PC11,GPI_DOWN,HIGH); //输入下拉
PCout(11)=0;
AFIO->MAPR &=~(0x03<<4); //位5~4清零
AFIO->MAPR |= 0x01<<4; //USART3部分重映射
}
else if(USART3_RX == PD9)
{
gpio_init(PD9,GPI_DOWN,HIGH); //输入下拉
PDout(9)=0;
AFIO->MAPR &=~(0x03<<4); //位5~4清零
AFIO->MAPR |= 0x03<<4; //USART3完全重映射
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
#if EN_USART3_RX
USARTx[usartn]->CR1|=1<<8; //位8: PEIE 校验中断使能 校验错误,硬件PE位置1
USARTx[usartn]->CR1|=1<<5; //位5: RXNEIE 使能接收中断
#endif
#if EN_USART3_TX
USARTx[usartn]->CR1|=1<<6; //位6:TCIE 发送完成中断使能位 当SR中的TC硬件置1,将产生中断
#endif
break;
//***********************************************************UART4***********************************************
case UART_4:
RCC->APB1ENR |= 1<<19; //使能UART4时钟
RCC->APB1RSTR |= 1<<19; //复位UART4
RCC->APB1RSTR &=~(1<<19); //停止复位
if(UART4_TX == PC10)
{
gpio_init(PC10,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
if(UART4_RX == PC11)
{
gpio_init(PC11, GPI_DOWN,HIGH); //输入下拉
PCout(11)=0;
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
#if EN_UART4_RX
USARTx[usartn]->CR1|=1<<8; //位8: PEIE 校验中断使能 校验错误,硬件PE位置1
USARTx[usartn]->CR1|=1<<5; //位5: RXNEIE 使能接收中断
#endif
#if EN_UART4_TX
USARTx[usartn]->CR1|=1<<6; //位6:TCIE 发送完成中断使能位 当SR中的TC硬件置1,将产生中断
#endif
break;
//***********************************************************USART1***********************************************
case UART_5:
RCC->APB1ENR |= 1<<20; //使能UART5时钟
RCC->APB1RSTR |= 1<<20; //复位UART5
RCC->APB1RSTR &=~(1<<20); //停止复位
if(UART5_TX == PC12)
{
gpio_init(PC12,GPO_MULPUSH_PULL_50,HIGH); //复用推挽输出,最大速度50MHZ
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
if(UART5_RX == PD2)
{
gpio_init(PD2, GPI_DOWN,HIGH); //输入下拉
PDout(2)=0;
}
else
{
ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
}
#if EN_UART5_RX
USARTx[usartn]->CR1|=1<<8; //位8: PEIE 校验中断使能 校验错误,硬件PE位置1
USARTx[usartn]->CR1|=1<<5; //位5: RXNEIE 使能接收中断
#endif
#if EN_UART5_TX
USARTx[usartn]->CR1|=1<<6; //位6:TCIE 发送完成中断使能位 当SR中的TC硬件置1,将产生中断
#endif
break;
}
//***************************************************************************************************************
USARTx[usartn]->BRR=mantissa; //波特率设置
USARTx[usartn]->CR1|=1<<2; //位2:RE 接收使能
USARTx[usartn]->CR1|=1<<3; //位3:TE 发送使能
USARTx[usartn]->CR1&=~(1<<12); //位12:M 定义字长0:8位 1:9位
//USARTx[usartn]->CR3|=1<<7; //位7: DMAT使能发送
//USARTx[usartn]->CR3|=0<<6; //位6: DMAR禁止接收
USARTx[usartn]->CR1|=1<<13; //位13:UE 串口使能
}
2.4.2 串口接收中断
当开启串口接收中断时,如果接收到数据,串口状态寄存器RXNE
位被设置为1;接着就可以从数据寄存器读取到接收到的数据;
#define USART_RX_LEN 1024 //定义接收到的字符串最大长度 最大不超过2^14-1=16383
u16 USART1_RX_STA = 0 ; //自定义接收状态寄存器
u16 USART2_RX_STA = 0 ; //自定义接收状态寄存器
u16 USART3_RX_STA = 0 ; //自定义接收状态寄存器
u16 UART4_RX_STA = 0 ; //自定义接收状态寄存器
u16 UART5_RX_STA = 0 ; //自定义接收状态寄存器
volatile USART_TypeDef *USARTx[5] = {USART1,USART2,USART3,UART4,UART5}; //定义五个指针数组保存 USARTx 的地址
/***********************************************************************************************************************
*
* Description: USART1中断函数
* USARTx->SR(32) : 接收状态寄存器 7:TXE 6:TC 5:RXNE
发送缓冲区为空 发送完成标志位 接收到数据标志位(读取DR则自动清0)
USARTx->DR(32) : 接收/发送数据缓冲区 只使用低8位
当接收到回车(回车由两个字节组成回车符0x0D和换行符0x0A),表示一次接收数据完成
*
**********************************************************************************************************************/
void USART1_IRQHandler(void)
{
u8 temp;
if(USART1->SR&(0x1<<5)) //接收到数据
{
temp = USART1->DR;
if((USART1_RX_STA&0x8000)==0) //接收数据未完成
{
if(USART1_RX_STA&0x4000) //已经接收到了0xOD
{
if(temp!=0x0A) //接收到的数据有误
USART1_RX_STA=0; //状态寄存器清零
else
USART1_RX_STA |=0x8000; //接收完成了
}
else //没有接收到0xOD
{
if(temp == 0x0D) //接收到回车符第一个字符 0xOD
{
USART1_RX_STA|=0x4000;
}
else //没有接收到0xOD
{
USART1_RX_BUF[USART1_RX_STA&0x3FFF]=temp; //保存数据
USART1_RX_STA++;
if(USART1_RX_STA>(USART_RX_LEN-1))
USART1_RX_STA=0; //接收数据超过USART_RX_LEN,则丢失数据,重新接收
}
}
}
}
/*******************************************************************************************/
//可在这里自定义其他任务
/********************************************************************************************/
}
在上面代码中, 当接收到回车(回车由两个字节组成回车符0x0D
和换行符0x0A
),表示一次接收数据完成,接收到的数据存放在字符数组USART1_RX_BUF
。
/***************************************************************************************************************
*
* USARTx_RX_BUF[USART_RX_LEN]:保存接收的数据
* USARTx_RX_STA :自定义16位接收状态寄存器
bit15 bit14 bit13~0
接收完成标志 接收到0xOD标志 接收到有效数据的个数
* 当接收到回车(回车由两个字节组成回车符0x0D和换行符0x0A),表示一次接收数据完成
*
**************************************************************************************************************/
u8 USART1_RX_BUF[USART_RX_LEN]; //接收缓冲区
u8 USART2_RX_BUF[USART_RX_LEN]; //接收缓冲区
u8 USART3_RX_BUF[USART_RX_LEN]; //接收缓冲区
u8 UART4_RX_BUF[USART_RX_LEN]; //接收缓冲区
u8 UART5_RX_BUF[USART_RX_LEN]; //接收缓冲区
2.4.3 串口发送
/***************************************************************************************************************
*
* 函数名称:uart_putchar
* 功能说明:串口发送一个字节
* 参数说明:USARTn 模块号(USART_1~USART_5)
* 函数返回:无
*
***************************************************************************************************************/
void usart_putchar (USARTn usartn, char ch)
{
//等待发送缓冲区空
while(!(USARTx[usartn]->SR&(1<<7))); //等待TXE=1 发送缓冲区为空 数据已转到移位寄存器
//发送数据
USARTx[usartn]->DR=ch;
// while(!(USARTx[usartn]->SR&(1<<5))); //等待TC=1,发送完成
}
/****************************************************************************************************************
*
* 函数名称:uart_sendStr
* 功能说明:串行发送字符串
* 参数说明:USARTn 模块号(USART_1~USART_5)
* str 字符串
* 函数返回:无
*
*************************************************************************/
void usart_sendStr (USARTn usartn, const u8 *str)
{
while(*str)
{
usart_putchar(usartn, *str++);
}
}
/****************************************************************************************************************
*
* 函数名称:uart_sendNum
* 功能说明:串行发送字符串
* 参数说明:USARTn 模块号(USART_1~USART_5)
* num 无符号整数
* 函数返回:无
*
*************************************************************************/
void usart_sendNum(USARTn usartn , u32 num)
{
u8 ch[32];
u8 i = 0;
IntToStr(num, ch);
while(ch[i])
{
usart_putchar(usartn, ch[i++]);
}
}
2.4.4 重定向printf
函数
重定向这个C
库的printf
函数,修改printf
函数底层调用fputc
:
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
/***************************START********************************************************************/
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};
/* FILE is typedef’ d in stdio.h. */
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{
x = x;
}
//重定义fputc函数 默认使用的是USART1口
int fputc(int ch, FILE *f)
{
USART1->DR = (u8) ch;
while((USART1->SR&0x40)==0); //循环发送,直到发送完毕 TC=1
return ch;
}
#endif
/***************************END*******************************************************************************/
2.4.5 输出接收数据
如需使能串口1接收数据,我们只需要在main
函数添加如下代码:
STM32_NVIC_Init(2,USART1_IRQn,0,1); //必须配置,其中包括中断使能
usart_init(USART_1,115200); //串口1初始化,波特率9600 映射到PA9 PA10
while(1)
{
// 数据接收完成
while(USART1_RX_STA&0x8000)
{
// 遍历数据
for(i=0;i<(USART1_RX_STA&0x3FFF);i++)
{
usart_putchar(USART_1,USART1_RX_BUF[i]);
}
usart_putchar(USART_1,'\n');
// 清空接收标志位
USART1_RX_STA=0;
}
delay_ms(1500);
}
2.5.6 烧录测试
编译完成后通过ISP
烧录到开发版,DTR
的低电平复位,RTS
高电平进BootLoader
。
由于ISP
烧录使用的也是串口1,因此可以使用usb
线连接到开发板ISP
接口,然后打开串口助手。即可通过ISP
接口与串口1进行数据交互;
这里有几点需要注意:
- 如果我们发送的是
HEX
,一行数据的结尾我们需要追加0x0D
(\r
),0x0A
(\n
); - 中断程序不能执行耗时长的程序,否则可能导致后面的数据到达的时候无法接受,会造成只接收到一个/两个字节的现象;因此需要避免在
USART1_IRQHandler
中使用printf
函数。
三、源码下载
源码下载路径:stm32f103
。
参考文章
[1] STM32F103
时钟系统讲解