STM32串口通信(STM32F103/STM32F407)
1.GPIO引脚复用AF机制 2.模块Clock时钟树,使能机制。(低功耗) 3.UART串口通信机制 4.NVIC中断配置机制 5.DMA搬移机制 6.Freertos串口
选择USART RX TX 引脚
- GPIO 口复用 机制
微控制器 I/O 引脚通过一个复用器连接到板载外设/模块,该复用器一次仅允许一个外设的复 用功能 (AF) 连接到 I/O 引脚。这可以确保共用同一个 I/O 引脚的外设之间不会发生冲突。 进行配置: ● 完成复位后,所有 I/O 都会连接到系统的复用功能 0 (AF0)。 ● 外设的复用功能映射到 AF1 至 AF13。 ● Cortex?-M4F EVENTOUT 映射到 AF15
PA9 PA10 复用端口的意义是SOC外设或者系统的模块的i/o功能有多路,需要配置到具体的引脚输出 引脚只是模块的输出通路,是工具人。 端口复用在datesheet中有描述,什么模块的端口事实上是指定的 PA9的复用AF7是串口TX。AF7系列是USART和I2S系列 引脚。 USART1 TX,RX的其中一条通路是PA9,PA10 另外一条通路是PB5 ,PB7
对于模块而言其功能的实现有多个PIN通路
对于PIN而言,他可能被多个模块链接
PIN越多可以同时实现模块功能就越多,复用通路只有一条,一但被选择那么其他的通路就无法联通。所以PIN越多,提供的外设能力就越大。
本节关键 Alternate function mapping 复用端口的配置 void GPIO_PinAFConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_PinSource, uint8_t GPIO_AF)
实现USART功能的复用需要选择复用AF7
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
配置PA9,PA10的串口功能
2.模块时钟使能 三条总线AHB,APB1,APB2,模块挂在相应的总线上。没有用到的模块就不需要开启,所以每个模块都有单独的时钟使能。 挂载哪条总线上就有哪个控制/管理 AHB1所管理的模块 AHB2 所管理的模块 APB管理的设备 APB2 管理的设备时钟 数据手册系统架构章节,对每条总线上的设备进行了列表,可以直接查询。 多数情况下,在库函数里面查看会更加方便 3.串口通信机制
UART 模块的输入和输出
TX发送口,RX接收口
发送的数据从TX发出从RX口接收。所以数据要发送通过本模块的TX。数据要接收通过本模块的RX。 所以两个串口设备的连线入上图所示。 人说话,从口出,听声音从耳入。 我说话,从口出,声音进别人耳朵,别人听到。 逻辑非常清晰。线不能链接错
MCU串口模块属于CPU外设。在MCU片内,电平TTL机制 TTL电平范围小,不利于长距离传输,可以通过提高传输电压提高抗干扰能力常用RS232电平。 电平机制的转换一般MCU无法实现,所以需要中间的转换IC 线路的联通需要物理接口,所以又会有接口标准 接口和电平会随着时代的发展而变化,和使用的场景关系非常大。 很显然他们都是串口通信的一部分。所以通信具有分层机制 接口和电平属于物理层。
传输 起始位,告知接收方数据开始发送,原则是和无信号的时候要有区别。约定无信息传递时高电平。开始传递时给低电平,表示开始 开始位S本身不不带有传输的信息,是告知对方“后面数据来了,请开始接收” 可以互相约定数据一次传输多少位 可以为5,6,7,8一般而言选择8位,代表一个字节。 发送完成数据后,需要表示传输完成,那么前面约定无信息传递时高电平。所以配备一个停止位T。 就这样重复的一个字节一个字节的传输。其实是一个Bit一个Bit传输。 为了适应跟多的情况,传输数据位可以配置,停止位也可以配置长度,需要双方约定好即可。
USART模块 可配置为 16 倍过采样或 8 倍过采样,因而为速度容差与时钟容差的灵活配置提供了可能 传输检测标志: — 接收缓冲区已满 — 发送缓冲区为空 — 传输结束标志
十个具有标志位的中断源: — CTS 变化 — LIN 停止符号检测 — 发送数据寄存器为空 — 发送完成 — 接收数据寄存器已满 — 接收到线路空闲 — 溢出错误 — 帧错误 — 噪声错误 — 奇偶校验错误
TX ,RX外部引脚
发送数据,由外部(CPU、DMA)写入数据到发送数据寄存器(TDR),按位发送到TX端口 接收数据,RX数据按位移入接收移位寄存器,然后写入RDR,可以由CPU或者DMA读走。 TDR,RDR都是8位的,所以读满就要取走 发送控制器,接收控制器分别控制模块的 发送和接收功能。 中断控制识别传输的各种事件,上报中断待处理。
当发送使能位 TE 置 1 之后,发送器开始会先发送一个空闲帧 (一个数据帧长度的高电平),接下 来就可以往 USART_DR 寄存器写入要发送的数据。在写入最后一个数据后,需要等待 USART 状 态寄存器 (USART_SR) 的 TC 位为 1,表示数据传输完成,如果 USART_CR1 寄存器的 TCIE 位置 1,将产生中断。 空闲帧
从起始位开始到结束位结束,测量时间大概87us 。波特率115200. 理论时间((1/115200 ) * 10 bit =86.8us) 这个时间87us是一帧的时间 空闲帧的开始是TX拉低的时候,也是大概87us 在发送数据时,编程的时候有几个比较重要的标志位我们来总结下。 如果将 USART_CR1 寄存器的 RE 位置 1,使能 USART 接收,使得接收器在 RX 线开始搜索 起始位。在确定到起始位后就根据 RX 线电平状态把数据存放在接收移位寄存器内。接收完成 后就把接收移位寄存器数据移到 RDR 内,并把 USART_SR 寄存器的 RXNE 位置 1,同时如果 USART_CR2 寄存器的 RXNEIE 置 1 的话可以产生中断。 为得到一个信号真实情况,需要用一个比这个信号频率高的采样信号去检测,称为过采样,这个 采样信号的频率大小决定最后得到源信号准确度,一般频率越高得到的准确度越高,但为了得到 越高频率采样信号越也困难,运算和功耗等等也会增加,所以一般选择合适就好。 在测量一个bit周期的时候,测量值8.91us 和理论时间8.68存在误差,这是由于采样频率造成的。频率越高就越能还原原始信号。 STM32F407 UART 可以进行16或者8倍的过采样频率。 传输完成或者接收完成需要通知外界,一般使用中断 也就是说,USART内部产生事件,要不要传递出去到NVIC中断控制器,由各自的使能位决定。
ARM cortex M4 NVIC中断控制器 USART 模块属于peripherals ,内部的事件使能后,以IRQs的形式传达给NVIC。
typedef struct
{
uint8_t NVIC_IRQChannel;
uint8_t NVIC_IRQChannelPreemptionPriority;
uint8_t NVIC_IRQChannelSubPriority;
FunctionalState NVIC_IRQChannelCmd;
} NVIC_InitTypeDef;
NVIC_IRQChannel
USART1_IRQn 上图称之为中断索引,当中断可以处理的时候,会通过索引去中断向量表上找到处理函数入口。
USART1 所有的中断,都是以USART1_IRQn的方式上报给NVIC。 USART内部的中断非常多,如何识别,通过 ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT)
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT)
{
uint32_t bitpos = 0x00, itmask = 0x00, usartreg = 0x00;
ITStatus bitstatus = RESET;
assert_param(IS_USART_ALL_PERIPH(USARTx));
assert_param(IS_USART_GET_IT(USART_IT));
if (USART_IT == USART_IT_CTS)
{
assert_param(IS_USART_1236_PERIPH(USARTx));
}
usartreg = (((uint8_t)USART_IT) >> 0x05);
itmask = USART_IT & IT_MASK;
itmask = (uint32_t)0x01 << itmask;
if (usartreg == 0x01)
{
itmask &= USARTx->CR1;
}
else if (usartreg == 0x02)
{
itmask &= USARTx->CR2;
}
else
{
itmask &= USARTx->CR3;
}
bitpos = USART_IT >> 0x08;
bitpos = (uint32_t)0x01 << bitpos;
bitpos &= USARTx->SR;
if ((itmask != (uint16_t)RESET)&&(bitpos != (uint16_t)RESET))
{
bitstatus = SET;
}
else
{
bitstatus = RESET;
}
return bitstatus;
}
这样可以获知USART内部具体是发生了什么事件而产生中断,在去做对应的处理。 NVIC所管理的称为全局中断。 NVIC的使能比较简单,因变对NVIC来说只有USART1_IRQn.不需要关注USART内部的各种事件。
NVIC_InitStructure_uart.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure_uart.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure_uart.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure_uart.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure_uart);
定义中断处理函数 在启动文件中 ; External Interrupts 写明了USART产生的中断服务函数 叫 USART1_IRQHandler 我们可以去修改这个函数名称,不过不建议这么做,保持S文件的原始性,也方便代码移植。 可以通过define的方式来定义中断服务函数。 一般来说,接收数据完成后会产生中断,这个时候把数据从寄存器中拿出来,放入我们准备好的缓存当中,这个时候数据是一个字节。 一个字节对于我们来说可能是一段话的而一部分,需要等待多个字节来组合一段有意义的内容。 所以把它占时放在缓存中 我们可以在缓存中对数据进行判断,识别数据包,或者一句话等等
unsigned char buff_index=0;
void USART1_IRQHandler(void)
{
uint8_t ucTemp;
if (USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET) {
ucTemp = USART_ReceiveData( USART1 );
UART1_BUFFER[buff_index] = ucTemp;
buff_index++;
if(buff_index > UART1_BUFFER_SIZE)
{
buff_index = 0;
memset(UART1_BUFFER,0,sizeof(UART1_BUFFER));
}
}
}
启用在线调试,观察UART1_BUFFER的内容。 0x0D(ascii码是13) 指的是“回车” \r是把光标置于本行行首 0x0A(ascii码是10) 指的是“换行” \n是把光标置于下一行的同一列
DMA数据搬移
串口的USART框图如图所示。数据寄存器(发送数据,接收数据寄存器)只能通过CPU或者DMA访问。 CPU访问数据寄存器的流程:数据寄存器接收满或者发送完毕会产生事件,事件传入NVIC中断模块后上报给CPU,CPU通过中断服务读取。这条链路在执行就会使用CPU,增加系统的负担。如果通过DMA可以在CPU不参与的情况下自己搬运数据,就好像不执行指令就能 做事一样。DMA模块本身的内容比较复杂,这里注重实现。 DMA的初始化结构体比较长,最后会附上dma的初始化代码。
DMA_InitTypeDef dma_init_struct; dma_init_struct.DMA_Channel = DMA_Channel_4;
DMA的信号通道选择和GPIO的复用结构有点相识。 DMA本身只管理STREAM0~STREAM7,只有这8条通路,然后STREAMx(x = 1,2,3,…8)通过多路复用器,和REQ_STRx_CH链接。 REQ_STRx_CH负责链接到具体的外设。
串口USART1 RX在数据流5(STREAM5)的通道4上。所以dma_init_struct.DMA_Channel = DMA_Channel_4; 初始化USART1 的代码如下所示
static void DMA_config(void)
{
DMA_InitTypeDef dma_init_struct;
extern unsigned char UART1_BUFFER[120];
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_DeInit(DMA2_Stream5);
while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE) {
}
dma_init_struct.DMA_Channel = DMA_Channel_4;
dma_init_struct.DMA_BufferSize = 120;
dma_init_struct.DMA_DIR = DMA_DIR_PeripheralToMemory;
dma_init_struct.DMA_FIFOMode = DMA_FIFOMode_Disable;
dma_init_struct.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
dma_init_struct.DMA_Memory0BaseAddr = (unsigned int)UART1_BUFFER;
dma_init_struct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
dma_init_struct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
dma_init_struct.DMA_PeripheralDataSize =DMA_PeripheralDataSize_Byte;
dma_init_struct.DMA_MemoryInc = DMA_MemoryInc_Enable ;
dma_init_struct.DMA_PeripheralInc =DMA_PeripheralInc_Disable ;
dma_init_struct.DMA_Mode =DMA_Mode_Circular;
dma_init_struct.DMA_PeripheralBaseAddr =(USART1_BASE+0x04);
dma_init_struct.DMA_PeripheralBurst =DMA_PeripheralBurst_Single;
dma_init_struct.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA2_Stream5, &dma_init_struct);
DMA_Cmd(DMA2_Stream5, ENABLE);
while (DMA_GetCmdStatus(DMA2_Stream5) != ENABLE) {
}
}
把外围 设备的数据放到内存的BUFFER
dma_init_struct.DMA_DIR = DMA_DIR_PeripheralToMemory;
第一步把DR数据寄存器的数据取出来 1.DR寄存器地址
dma_init_struct.DMA_PeripheralBaseAddr =(USART1_BASE+0x04);
2.一次取多少数据,总共取多少次,取满次数怎么办
dma_init_struct.DMA_PeripheralDataSize =DMA_PeripheralDataSize_Byte;
dma_init_struct.DMA_BufferSize = 120;
dma_init_struct.DMA_Mode =DMA_Mode_Circular;
3。下一次取要不要取另外地址的数据
dma_init_struct.DMA_PeripheralInc =DMA_PeripheralInc_Disable ;
第二部 DMA获得数据 第三部 DMA写入数据到目标内存
1.内存地址
dma_init_struct.DMA_Memory0BaseAddr = (unsigned int)UART1_BUFFER;
2.写入地址会不会变化
dma_init_struct.DMA_MemoryInc = DMA_MemoryInc_Enable ;
根据以上的逻辑可以理清楚DMA结构体初始化的过程。 最后在主函数中使能 UART DMA
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
1.对面发送端的TX完成了一帧的数据传输,USART 接收完成标志RXNE置位, 2.DMA收到请求源请求 3,读取USART DR寄存器数据值 关掉CPU搬移数据。 最后开启调试,全速运行。
操作系统(Freertos)下的UART OS下的USART
UART的数据接收是通过硬件进行,硬件没有缓存,所以一但接收数据寄存器满,就要及时的取出来 这个时候有两种方法 1.NVIC 中断服务函数处理
void USART1_IRQHandler(void)
{
#if 1
uint8_t ucTemp;
if (USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET) {
ucTemp = USART_ReceiveData( USART1 );
UART1_BUFFER[buff_index] = ucTemp;
buff_index++;
if(buff_index > UART1_BUFFER_SIZE)
{
buff_index = 0;
memset(UART1_BUFFER,0,sizeof(UART1_BUFFER));
}
}
#endif
}
2.使用DMA处理
数据从DR寄存器拿出来后放入指定的缓存,这个时候是否要通知CPU来对数据进行判别 数据是否可用。串口UART是基本的通信,它只有帧的概念 也就是起始位和结束位之间的数据。 没有数据包的概念。起始位和结束位之间的数据一般是一个字节 这是UART协议简单的缘故,应为简单所以普及。视乎有点矛盾。 其他的通信协议只是在这套基础上加入一些逻辑信息。 那么我们可以给串口加入 帧的概念 通信是 接收方和发送方的数据交互,所以都需要约定好。意思就是说,发送方接收方都要加入帧的概念。
我们约定一个字符 # 作为帧的开始 字符 &作帧结束。或者其他的约定好的字符都可以。 这样我们在OS中可以在适当的时候做帧识别。关键在于串口信息事件处理的实时性要求高不高。 嵌入式实时性一般摆在第一位,也就是说,如果优先级高就放在中断做,这样响应快 如果不是那么非常重要就可以用DMA搬运到BUFFER 用信号量或者互斥量去唤醒帧识别 函数处理任务。
void uart_rx_isr(uart_device_t *dev)
{
int32_t ch = -1;
if (!dev)
{
return;
}
hw_interrupt_disable();
while (1)
{
ch = dev->ops->getc(dev);
if (ch == -1)
{
break;
}
ch &= 0xFF;
sw_fifo_put(&dev->rx_fifo, (const uint8_t *)&ch, 1, SW_FIFO_TYPE_COVER);
if (dev->rx_indicate.cb)
{
dev->rx_indicate.cb(dev->rx_indicate.para);
}
}
hw_interrupt_enable();
}
dev->rx_indicate.cb是一个回调函数,当接收数据后执行一个函数,这个时候可以做数据的处理或者发送信号量。
|