一.USART寄存器概述
STM32的USART(通用同步异步收发器)模块通过寄存器配置实现串行通信。关键寄存器包括控制寄存器(CR1/CR2/CR3)、状态寄存器(SR)、数据寄存器(DR)、波特率寄存器(BRR)等。直接操作寄存器可精细控制通信参数和状态。
二.USART框图
三.USART各寄存器功能分析
USART_SR寄存器
此寄存器包括发送完成标志位,发送寄存器为空标志位,接收标志位为空标志位,以及空闲标志位
注意:IDLE标志位/TC的清除需要先读SR寄存器再读DR寄存器(有先后顺序)才能完成idle标志位清除。
在USART_DR寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的 传输结束。
当一帧发送完成时(停止位发送后)并且设置了TXE位,TC位被置起,如果USART_CR1寄存器 中的TCIE位被置起时,则会产生中断。
USART_CR1寄存器
此寄存器包括usart使能,发送使能,接收使能,校验位选择,各种中断使能,
TE发送使能从0变成1后会发送一个空闲帧
注意:UE位具备写保护,完成配置之后再打开!!!!!!!!!!!!!!!
USART_CR2寄存器
此寄存器可配置停止位,使能时钟,配置时钟极性等等
USART_CR3寄存器
可以配置使能DMA发送或者接收
USART_BRR寄存器
波特率由BRR寄存器设置,计算公式如下:
- USARTDIV = fplckx / (16 × BaudRate)
- BRR = (整数部分 << 4) | (小数部分 × 16)
例如,当PCLK为72MHz,目标波特率为115200时:
USARTDIV = 72000000 / (16 × 115200) ≈ 39.0625
BRR = (39 << 4) | (0.0625 × 16) = 0x273
四.UASRT通信配置流程
1.开启GPIO和USART1时钟
GPIO和USART1都是APB2的外设,通过RCC_APB2ENR寄存器使能这两个外设。
#define RCC_APB2 (0x40021000+0x18)
reg=(unsigned int*)RCC_APB2; //存放RCC_APB2地址
*reg |= (1<<2); //使能GPIOA
*reg |= (1<<14); //使能USART1
2.配置gpio
/* 串口引脚配置 */
reg=(unsigned int*)GPIOA_CRH;
//配置PA9->TX
*reg &= ~(0x0F<<4); //先清零,方式赋值错误
*reg |= (0x03<<4); //输出模式,speed=50Mhz
*reg |= (0x02<<6); //复用推挽,speed=50Mhz
//配置PA10->RX
*reg &= ~(0x0F<<8); //先清零,方式赋值错误
*reg |= (0x00<<8); //输入模式
*reg |= (0x01<<10); //浮空输入,speed=50Mhz
3.usart寄存器配置
先通过结构体模拟寄存器的位置分布,再通过给结构体指针传入USART1基址来使结构体成员和usart1寄存器一一对应
typedef struct
{
volatile uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
volatile uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
volatile uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
volatile uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
volatile uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
volatile uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
volatile uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;
USART_TypeDef *usart1 = (USART_TypeDef *)USART1_BASIC;
/****** 配置波特率,自己计算 *******/
/* CLK=8Mhz,波特率115200计算 */
/*
8000000/16/USARTDIV=115200 USARTDIV=4.34
*/
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction);
usart1->CR1 |= (0x00<<12); //选择8位数据位
usart1->CR1 |= (0x01<<3); //设置RE,接受使能
usart1->CR1 |= (0x01<<2); //设置TE,发送使能
usart1->CR2 &= ~(3<<12); //设置停止位为1
usart1->CR1 |= (0x01<<13) //设置UE串口使能
到这里就ok了,可以接收或者发送数据了。
int getchar(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<5)) == 0); //死等RXNE
return usart1->DR; //读DR操作可以清除RXNE
}
int putchar(char c)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<7)) == 0); //读TEX(读SR寄存器)
usart1->DR = c; //写入DR(同时清除TC和TXE)
return c;
}
4.USART收发消息实验:
.s启动文件
没有设置STM32的时钟,默认用的内部8Mhz的时钟
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY ;定义只读数据段,标记为RESET,其实放在CODE区,位于0地址
EXPORT __Vectors ;在程序中声明一个全局的标号__Vectors,该标号可在其他的文件中引用
__Vectors DCD 0 ; Top of Stack:当前地址写入一个32bit数据,值为0x00000000,实际是应该填入栈顶地址
DCD Reset_Handler ; Reset Handler:当前地址写入一个字32bit数据,值为Reset_Handler的值,即程序入口地址
AREA |.text|, CODE, READONLY ;定义代码段,标记为.text
; Reset handler ;利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰
Reset_Handler PROC ;过程的开始
EXPORT Reset_Handler [WEAK] ;[WEAK] 弱定义,意思是如果在别处也定义该标号(函数),在链接时用别处的地址。
IMPORT main ;声明main函数
LDR SP, =(0x20000000+0x5000) ;设置栈,因为main是C函数
BL main
ENDP ;过程的结束
END ;整个汇编文件结束
int main()
{
char c;
USART_Init();
putchar('1');putchar('0');putchar('0');putchar('a');putchar('s');putchar('k');putchar('\n');putchar('\r');
while(1)
{
c = getchar();
putchar(c);
putchar(c+1);
}
return 0;
}
实验现象如下:
一开始向上位机发送100ask, 上位机发送'A', STM32发送'B'
五.USART通信&中断
1.简介:
中断的配置应分为三部分:第一部分usart的中断使能,EXIT的的
常用的就是第4位的空闲中断和第5位的接收完成中断;第4位的中断信号是在接收完成后一段时间内再也没有数据接收了就会被触发,所以叫做空闲中断的使能;而第5是接收中断,也就是每次接收完一个字符后就会触发一次中断。
2.IDLE中断和RXNE中断简介
STM32的IDLE的中断在串口无数据接收的情况下不会一直产生,产生的条件是,当清除IDLE标志位后,必须有接收到第一个数据后,且接收完成后等待一个数据帧的时间,此时间内RX一直为'1',才开始触发。就是接收的数据断流,没有接收到数据,即产生IDLE中断。单字节接收时RXNE和IDLE开启一个就行,减少进中断的次数。
空闲符号被视为完全由’1’组成的一个完整的数据帧,后面跟着包含了数据的下一帧的开始位(‘1’ 的位数也包括了停止位的位数)。
USART不属于外部中断的范畴,无需配置EXIT寄存器。直接配置NVIC即可。
NVIC的配置涉及1.中断使能(需要找到其中断号) 2.相应中断中断优先级配置(也需要其中断号) 3.中断优先级分组(不在NVIC中)
3.USART收发消息+中断实验
usart1/gpio/nvic配置:
void USART_Init(void)
{
/* 存放寄存器地址的变量-reg */
unsigned int *reg;
/*使能usart1,GPIOA-PIN9,PIN10*/
reg=(unsigned int*)RCC_APB2; //存放RCC_APB2地址
*reg |= (1<<2); //使能GPIOA
*reg |= (1<<14); //使能USART
/* 串口引脚配置 */
reg=(unsigned int*)GPIOA_CRH;
//配置PA9->TX
*reg &= ~(0x0F<<4); //先清零,方式赋值错误
*reg |= (0x03<<4); //输出模式,speed=50Mhz
*reg |= (0x02<<6); //输出模式,speed=50Mhz
//配置PA10->RX
*reg &= ~(0x0F<<8); //先清零,方式赋值错误
*reg |= (0x00<<8); //输入模式
*reg |= (0x01<<10); //输出模式,speed=50Mhz
/* 串口传输配置 */
//设置波特率,BRR寄存器
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction);
//设置数据格式,以及使能
usart1->CR1 |= (0x01<<13)|(0x00<<12)|(0x00<<10)|(0x01<<3)|(0x01<<2);
usart1->CR2 &= ~(3<<12); //设置停止位
//设置中断使能
usart1->CR1 |= (0X01<<5); //开启RXNEIE中断
usart1->CR1 |= (0X01<<4); //开启IDLE中断
NVIC->ISER[(37/32)] |= (0x01<<(37%32)); //使能中断
/** 只有一个中端所以SCB_AIRCR寄存器配置中断分组和NVIC->IP寄存器配置优先级都可以不用管 **/
}
中断处理函数:
/************* 抽象层函数 ***************/
int getchar(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<5)) == 0);
return usart1->DR;
}
int putchar(char c)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<7)) == 0);
usart1->DR = c;
return c;
}
/************** 应用层函数 ****************/
void my_printf(const char * buff)
{
unsigned char count=0;
while(buff[count])
{
putchar(buff[count++]); //把接收到的数据再发回去
}
putchar('/r');
putchar('/n');
}
/*********** 中断处理函数 ****************/
void USART1_IRQn(void)
{
static unsigned char count=0;
if(usart1->SR & (0x01<<5)) //RXNE中断产生
{
buff[count++]=usart1->DR; //读寄存器会清除RXNE标志位
}
if(usart1->SR & (0x01<<4)) //IDLE中断产生
{
buff[count]=usart1->DR; //读寄存器会清除IDLE标志位
buff[count]='0'; //字符串结束标志位
count=0; //读完数据后count清零
}
}
六.USART+DMA通信方式
1.DMA简介
DMA干嘛的?上面我们的USART数据再DR寄存器到某个全局数组的过程由CPU完成,而CPU的资源是很珍贵的,数据数据转运用DMA可以绕过CPU,节省CPU资源。
DMA三种情况的数据传输如下:外设到内存,内存到外设,内存到内存(只需要配置源地址,传输多少数据,传输几次),当剩余传输数据量不是0,而且DMA是启动状态,那么就会发生数据传输(重点是什么时候唤醒DMA)。
每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;
- 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
- 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
- 支持循环的缓冲器管理;
- 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;
- 存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输;
- 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;
- 可编程的数据传输数目:最大为65535。
参考:【STM32】 DMA原理,步骤超细详解,一文看懂DMA-CSDN博客
从外设(TIMx[x=1、2、3、4]、ADC1、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3]) 产生的7个请求,通过逻辑或输入到DMA1控制器,这意味着同时只能有一个请求有效。
DMA传输过程:
- 外设发出请求(一个事件)
- 若是同时还有别的请求,DMA依据优先权响应。
- DMA访问发出请求的外设,会给一个应答信号。
- 外设得到应答信号,立刻释放请求,DMA撤销应答信号。
- DMA开始传输。
DMA仲裁器:
- 软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级
- 硬件:优先级已经被确定了,参考上图。
- 请求软件优先级相同时,比较硬件的优先级。
DMA有三个标志位,分别是半传输标志位,传输完成标志位,传输错误标志位。
在传输计数器的右边有一个自动重装器,这个自动重装器的作用就是传输计数器减到零之后,是否要自动恢复到最初的值,比如最初传输计数器置5,如果不使用自动重装器,那转运5次后DMA就结束了,如果使用自动重装器,那转运5次计数器减到零后就会立即重装到初始值5,这个就是自动重装器,它决定了转运的模式。
如果不重装就是正常的单次模式,如果重装就是循环模式,如果想转运一个数组,那一般就是单次模式转运一轮就结束了,如果是ADC扫描模式加连续转换,为了配合ADC,DMA也需要使用循环模式,所以这个循环模式和ADC的连续模式差不多,都是指定一轮工作完成后,是不是立即开始下一轮工作。
触发就是决定DMA 需要在什么时机进行转运的。触发源有硬件触发和软件触发,具体选择哪个由M2M这个参数决定, M2M就是 memory to memory 即存储器到存储器的意思。
当我们给M2M位1时,DMA就会选择软件触发,这个软件触发并不是调用某个函数一次触发一次,这个软件触发的执行逻辑是以最快的速度连续不断地触发DMA,尽快把传输计数器清零,完成这一轮的转换。所以这里的软件触发和之前外部中断和ADC 的软件触发可能不太一样,也可以把它理解成连续触发。
这个软件触发和循环模式不能同时用,因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,那DMA就停不下来了。
软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运,是软件启动不需要时机,并且想尽快完成的任务。
当我们给M2M位0时,那就是使用硬件触发,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,所以需要使用硬件触发,在硬件达到这些时机时,传个信号过来来触发DMA 进行转运。
单次模式或者normal模式就是存储器到存储器模式,只进行一次转运。
数据传输宽度、对齐方式和数据大小端
三种情况:
- 源端宽度和目标宽度相同,source=0x88,target=0x88
- 源端宽度是目标宽度一半,source=0x88,target=0x0088(会被写入低地址)
- 源端宽度是目标宽度两倍,source=0x8888,target=0x88(低位会被写入,高位被舍弃)
2.DAM寄存器
(1).DMA中断状态寄存器(DMA_ISR)
- 每个通道的中断状态用4个位表示,分别是通道x传输错误中断标志,半传输中断标志,传输完成中断标志,全局中断标志
- 中断标志位的清除:在DMA_IFCR寄存器的相应位写入’1’可以清除这里对应的标志位(和exit挂起寄存器标志位清零的操作一样)。
(2).DMA中断标志清除寄存器(DMA_IFCR)
- 用于清除ISR中的标志位
(3).DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)-------很重要
DMA模式,中断使能,数据传输宽度,优先级,数据方向,通道开启都在这个寄存器里面配置
(4).DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)
DMA通道x外设地址寄存器/存储器地址寄存器
5.DMA转运条件
DMA进行转运有几个条件:
- 第一就是开关控制,DMACmd必须使能
- 第二就是传输计数器必须大于0
- 第三就是触发源必须有触发信号
触发一次转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发, DMA都不会再进行转运了, 此时就需要 DMACmd给DISABLE, 关闭DMA, 再为传输计数器写一个大于0的数, 再调用DMACmd给ENABLE开启DMA, DMA才能继续工作。
注意的是写传输计数器时,必须要先关闭DMA再进行,不能在DMA开启时写传输计数器,这是手册里的规定。
3.示例
补充:
(1).存储器到存储器
DMA.c文件
typedef struct
{
__IO uint32_t CCR;
__IO uint32_t CNDTR;
__IO uint32_t CPAR;
__IO uint32_t CMAR;
} DMA_Channel_TypeDef;
DMA_Channel_TypeDef *
DMAy_Channelx = 0x40020008; //通道一寄存器基址
void DMA_Init(void * src,void * tar)
{
//别忘了开启DMA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
uint32_t tmpreg=0;
/******** 配置CCR寄存器 *********/
tmpreg = DMAy_Channelx->CCR; //读出CCR寄存器的值
tmpreg &= 0xFFFF800F; //清除14~4bit
tmpreg |= (0x01<<14); //选用单次模式(MEM2MEM)
tmpreg |= (0x01<<12); //配置为中优先级
tmpreg |= (0x00<<10); //存储器数据宽度8bit
tmpreg |= (0x00<< 8); //外设数据宽度8bit
tmpreg |= (0x01<< 7); //存储器地址自增
tmpreg |= (0x01<< 6); //外设地址自增
tmpreg |= (0x00<< 5); //不执行循环
tmpreg |= (0x00<< 4); //从外设读->写入存储器
DMAy_Channelx->CCR = tmpreg; //写入寄存器
//CCR寄存器第零位为EN,先不开启,开启后无法设置计数器
/*------------------------------------------------------------------------*/
/**** 配置CNDTRx CPARx,CMARx寄存器 ***/
DMAy_Channelx->CNDTR = 4; //转运的数据大小(计数器/转运次数)
DMAy_Channelx->CPAR = PeripheralBaseAddr; //外设地址
DMAy_Channelx->CMAR = MemoryBaseAddr; //存储器地址
/*------------------------------------------------------------------------*/
}
void DMA_Enable(void)
{
DMAy_Channelx->CCR |= 0x01; //设置EN位,开启DMA
}
void DMA_Disable(void)
{
DMAy_Channelx->CCR &= ~0x01; //设置EN位,关闭DMA
}
main.c文件
uint8_t src[4] = {1,2,3,4};
uint8_t target[4] = {0};
int main()
{
OLED_Init();
SRC_Show(); //显示源地址数据
TAR_Show(); //显示目标地址数据
DMA_Init((uint32_t)src,(uint32_t)target); //配置通道一
void SRC_Show(); //开启DMA
Delay_ms(2000);
TAR_Show();
while(1);
}
void SRC_Show(void)
{
for (uint8_t i = 0; i < 4 ;i++)
{
OLED_ShowNum(2,i+1,src[i],1);
}
}
void TAR_Show(void)
{
for (uint8_t i = 0; i < 4 ;i++)
{
OLED_ShowNum(3,i+1,target[i],1);
}
}
实验现象应为一开始显示1234,0000,2s后第二行从0000,变为1234,说明转运成功!
若是src数据发生变化先要第二次转运
- 关闭DMA,CCR寄存器的EN位设置为0
- 重写CNDTR寄存器,重置计数值
- 开启DMA,DMA开始转运
/*** 重置计数值,开启新一次转运 ***/
DMAy_Channelx->CCR &= ~0x01 //设置EN位,关闭DMA
DMAy_Channelx->CNDTR = 4; //转运的数据大小(计数器/转运次数)
DMAy_Channelx->CCR |= 0x01 //设置EN位,开启DMA
/*-----------------------------------------------------------------------------/
/*** 重置计数值,开启新一次转运 ***/
uint32_t * DMA_ISR = 0x4002 0000; //ISR寄存器地址
while(*DMA_ISR & 0x02 == 0) ; //传输完成才能进行下一步
* DMA_ISR &= ~0x02; //清除TCIF1标志位
void Restart_DMA(void)
{
/*** 重置计数值,开启新一次转运 ***/
DMAy_Channelx->CCR &= ~0x01; // 设置EN位,关闭DMA
DMAy_Channelx->CNDTR = 4; // 转运的数据大小(计数器/转运次数)
DMAy_Channelx->CCR |= 0x01; // 设置EN位,开启DMA
/*-----------------------------------------------------------------------------*/
/*** 重置计数值,开启新一次转运 ***/
uint32_t * DMA_ISR = (uint32_t *)0x40020000; // ISR寄存器地址
while (*DMA_ISR & 0x02 == 0); // 传输完成才能进行下一步
*DMA_ISR &= ~0x02; // 清除TCIF1标志位
/*-----------------------------------------------------------------------------*/
}
int main()
{
OLED_Init();
SRC_Show();
TAR_Show();
DMA_Init1((uint32_t)src,(uint32_t)target);
Delay_ms(2000);
TAR_Show();
Delay_ms(2000);
SRC_ADD();
SRC_Show();
Restart_DMA();
TAR_Show();
while(1)
{
}
}
(2)外设到存储器:USART+DMA
- uasrt的CR3.DMAR应该设为1
- DMA的CCR.MEM2MEM应设置为非存储器到存储器模式(如果设置为存储器到存储器模式会DMA一旦启动,不需要USART的事件触发,直接转运x次,而且这种读DR寄存器的方式不会清除RXNE标志位,就是DMA和USART除了从DR读数据没有其他联系了)
- USART的接收对应DMA1的通道五
.h文件
#define RCC_APB2 (0x40021000+0x18)
#define GPIOA_BASIC 0x40010800
#define GPIOA_CRL 0x40010800
#define GPIOA_CRH 0x40010804
#define GPIOB_BASIC 0X40010C00
#define GPIOC_BASIC 0x40011000
#define USART1_BASIC 0x40013800
#define DIV_Mantissa 39
#define DIV_Fraction 1
DMA_Channel_TypeDef * DMAy_Channelx = (DMA_Channel_TypeDef *)DMA1_Channel5;//(0x40020008+20*4); // 通道五寄存器基址
USART_TypeDef *usart1 = (USART_TypeDef *)USART1_BASIC;
usart部分代码:
void USART1_Init(void)
{
/* 存放寄存器地址的变量-reg */
unsigned int *reg;
/*使能usart1,GPIOA-PIN9,PIN10*/
reg=(unsigned int*)RCC_APB2; //存放RCC_APB2地址
*reg |= (1<<2); //使能GPIOA
*reg |= (1<<14); //使能USART
/*--------------------------------------------------------------*/
/* 串口引脚配置 */
reg=(unsigned int*)GPIOA_CRH;
//配置PA9->TX
*reg &= ~(0x0F<<4); //先清零,防止赋值错误
*reg |= (0x03<<4); //输出模式,speed=50Mhz
*reg |= (0x02<<6); //复用推挽
//配置PA10->RX
*reg &= ~(0x0F<<8); //先清零,防止赋值错误
*reg |= (0x00<<8); //输入模式,speed=50Mhz
*reg |= (0x01<<10); //输入模式
/*-------------------------------------------------------------*/
/* 串口传输配置 */
//设置波特率,BRR寄存器
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction); //115200
//设置数据格式,以及使能
usart1->CR1 &= ~(0x00<<12); //有效位8位
usart1->CR1 &= ~(0x00<<10); //禁止校验位
USART1->CR1 &= ~(0x1F0); //中断全部失能
usart1->CR1 |= (0x01<<3); //发送使能
usart1->CR1 |= (0x01<<2); //接收使能
/*-------------------------------------------------------------*/
/* 配置CR2,CR3 */
usart1->CR2 = 0;
usart1->CR2 &= ~(0x03<<12); //设置停止位
usart1->CR3 &= 0;
usart1->CR3 |= (0x01<<6); //RXNE事件触发DMA转运
usart1->CR1 |= (0x01<<13); //开启USART1
/*-------------------------------------------------------------*/
}
DMA部分代码:
void DMA_Init1(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
uint32_t tmpreg = 0;
/******** 配置CCR寄存器 *********/
tmpreg = DMAy_Channelx->CCR; // 读出CCR寄存器的值
tmpreg &= 0xFFFF800F; // 清除14~4bit
tmpreg |= (0x00 << 14); // 选用非存储器到存储器模式
tmpreg |= (0x01 << 12); // 配置为中优先级
tmpreg |= (0x00 << 10); // 存储器数据宽度8bit
tmpreg |= (0x00 << 8); // 外设数据宽度8bit
tmpreg |= (0x01 << 7); // 存储器地址自增
tmpreg |= (0x00 << 6); // 外设地址不自增
tmpreg |= (0x00 << 5); // 不执行循环
tmpreg |= (0x00 << 4); // 从外设读->写入存储器
DMAy_Channelx->CCR = tmpreg; // 写入寄存器
// CCR寄存器第零位为EN,先不开启,开启后无法设置计数器
/*------------------------------------------------------------------------*/
/**** 配置CNDTRx CPARx,CMARx寄存器 ***/
DMAy_Channelx->CNDTR = 32; // 转运的数据大小(计数器/转运次数)
DMAy_Channelx->CPAR = (uint32_t)&usart1->DR; // 外设地址
DMAy_Channelx->CMAR = (uint32_t)target; // 存储器地址
/*------------------------------------------------------------------------*/
DMAy_Channelx->CCR |= 0x01; // 开启DMA
}
测试程序:
int main()
{
OLED_Init();
USART1_Init();
OLED_ShowString(3,1,target);
Delay_ms(2000);
DMA_Init1();
uint8_t i=0;
while(1)
{
OLED_ShowNum(1,1,DMAy_Channelx->CNDTR & 0x0000FFFF,7);
OLED_ShowNum(2,1,usart->SR & (0x01<<5),6);
OLED_ShowString(3,1,target);
OLED_ShowChar(4,1,usart->DR);
}
return 0;
}
需注意的问题:
- 数据宽度一定要对齐,虽然文档上说不对齐也能正常读取,但实际中会出现各种奇怪的问题
- USART的DMAR位一定要设置,这是触发DMA转运的标志位
- DMA一定要设置成外设到存储器的模式,否则无需事件触发直接转运
- DMA的计数器设置一定要在开启之前,开启后写保护,若开启后需重写计数器,需要先关闭DMA
(3).USART+DMA+IDLE中断
配置步骤:
- USART,GPIO,DMA时钟配置
- USART配置:对应GPIO配置,USART配置,IDLE中断使能,串口使能
- DMA配置:数据宽度,模式,传输方向,DMA使能
- NVIC配置:UASRT IDLE中断开启,中断优先级配置
RCC配置:
RCC->APB2RSTR |= (1<<2); //使能GPIOA
RCC->APB2RSTR |= (1<<14); //使能USART
RCC->AHBENR |= (1<<0); //使能DMA
GPIO配置:
reg=(unsigned int*)GPIOA_CRH;
//配置PA9->TX
*reg &= ~(0x0F<<4); //先清零,防止赋值错误
*reg |= (0x03<<4); //输出模式,speed=50Mhz
*reg |= (0x02<<6); //输出模式,speed=50Mhz
//配置PA10->RX
*reg &= ~(0x0F<<8); //先清零,防止赋值错误
*reg |= (0x00<<8); //输入模式
*reg |= (0x01<<10); //输出模式,speed=50Mhz
USART配置:
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction); //波特率设置-115200
usart1->CR1 &= ~(0x00<<12); //数据宽度8位
usart1->CR1 &= ~(0x00<<10); //禁止校验位
USART1->CR1 &= ~(0x1F0); //中断全部失能
usart1->CR1 |= (0x01<<4); //IDLE中断使能
usart1->CR1 |= (0x01<<3); //发送使能
usart1->CR1 |= (0x01<<2); //接收使能
usart1->CR2 = 0;
usart1->CR2 &= ~(0x03<<12); //设置停止位
usart1->CR3 &= 0;
usart1->CR3 |= (0x01<<6); //RXNE事件触发DMA转运
usart1->CR1 |= (0x01<<13); //开启USART1
DMA配置:
uint32_t tmpreg = 0;
/******** 配置CCR寄存器 *********/
tmpreg = DMAy_Channelx->CCR; // 读出CCR寄存器的值
tmpreg &= 0xFFFF800F; // 清除14~4bit
tmpreg |= (0x00 << 14); // 选用非存储器到存储器模式
tmpreg |= (0x01 << 12); // 配置为中优先级
tmpreg |= (0x00 << 10); // 存储器数据宽度8bit
tmpreg |= (0x00 << 8); // 外设数据宽度8bit
tmpreg |= (0x01 << 7); // 存储器地址自增
tmpreg |= (0x00 << 6); // 外设地址不自增
tmpreg |= (0x00 << 5); // 不执行循环
tmpreg |= (0x00 << 4); // 从外设读->写入存储器
DMAy_Channelx->CCR = tmpreg; // 写入寄存器
// CCR寄存器第零位为EN,先不开启,开启后无法设置计数器
/*------------------------------------------------------------------------*/
/**** 配置CNDTRx CPARx,CMARx寄存器 ***/
DMAy_Channelx->CNDTR = 32; // 转运的数据大小(计数器/转运次数)
DMAy_Channelx->CPAR = (uint32_t)&usart1->DR; // 外设地址
DMAy_Channelx->CMAR = (uint32_t)target; // 存储器地址
/*------------------------------------------------------------------------*/
DMAy_Channelx->CCR |= 0x01; // 开启DMA
NVIC配置:
NVIC->ISER[(37/32)] |= (0x01<<(37%32)); //使能USART1中断
/* 由于此程序只有这一个中断,所以优先级和优先级分组可以先不配置 */
中断函数:
void USART1_IRQHandler(void)
{
uint32_t SR=0;
if(usart1->SR & (0x01<<4) || usart1->CR1 & (0x01<<4)) // IDLE中断产生
{
SR = usart1->SR;
SR = usart1->DR; // 清除IDLE中断标志位
DMAy_Channelx->CCR &= ~0x01; // 设置EN位,关闭DMA
DMAy_Channelx->CNDTR = 32; // 转运的数据大小(计数器/转运次数)
DMAy_Channelx->CCR |= 0x01; // 设置EN位,开启下一次数据转运
}
}
注意:USART的IDLE:监测到总线空闲 (IDLE line detected)标志位需要按顺序读SR,DR寄存器才能清除,不可以直接'与'操作清零
注意事项
- 修改寄存器前需确认USART处于禁用状态(UE位清零)。
- 波特率误差需控制在允许范围内(通常<2%)。
- 多字节传输时建议结合DMA减轻CPU负担。
通过直接操作寄存器,可灵活实现高效、低延时的串口通信,适合对实时性要求高的场景。