STM32之usart
Posted YellowMax2001
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32之usart相关的知识,希望对你有一定的参考价值。
1、uart与usart的区别
UART:universal asynchronous receiver and transmitter
通用 异步 接收 发送
[总线信号] TX , RX
USART:universal synchronous asynchronous receiver and transmitter
通用 同步 异步 接收 发送
[总线信号] TX, RX, CK
总体来说,usart只是比uart多了一个同步信号,usart可以使用同步方式进行信息的收发
同步传输与异步传输的特点:
同步传输
传输过程由主机的SCLK信号控制,在SCLK的一个上升沿或者下降沿到来的时候,一位数据的传输,不受时间长短限制,只与主机的SCLK信号有关。这个跟SPI协议很像异步传输
传输过程中标志一个数据的开始是一个起始位,通常是逻辑’0’。传输bit数据靠接收端的采样,这个是有严格的时间限定的,接收方与发送方必须采用严格的一致的发送与采样时间间隔,也就是说,信息的发送速度靠发送方与接收方共同决定。这个是我们通常用到的异步串口通讯
2、STM32F407ZG中usart的部分特性
usart模块框图:
- 可通过DMA实现高速度的数据传输
- 全双工, 异步
- NRZ 标准格式 (Mark/Space)
- 分步波特率发生系统
- 通用可编程波特率大小
- 可编程数据长度 (8 or 9 bits)
- 可配置的停止位,支持 1 or 2 停止位
- LIN 主机同步中断发送和 LIN 从机中断检测
- 当usart被硬件配置为 LIN模式的时候支持13-bit 中断产生和 10/11 bit 中断检测
- 发送端的时钟输出作为传输同步信号
- 单工半双工通信
传输检测标志:
– 接收缓冲区满
– 发送缓冲区空
– 传输结束
奇偶检验:
– 传输奇偶检验位
– 检查接收到数据的奇偶检验位
四个错误检测标志:
– 溢出错误
– 噪声检测
– 帧错误
– 奇偶检验错误
中断源:
– CTS 改变
– LIN 中断监测
– 发送数据寄存器空
– 传输结束
– 接收寄存器满
– 接收到线路空闲
– 溢出错误
– 帧错误
– 噪声错误
– 奇偶校验错误
3、功能描述
三个引脚:TX RX SW_RX
- TX:数据发送引脚,当被禁止的时候,该引脚会恢复到IO口配置,当发送器使能并且没有任何数据传输的时候,引脚保持高电平。在单线模式下该引脚用于接收与发送数据(在usart下,数据同时会被发送到SW_RX引脚)
- RX:数据接收引脚,过采样技术用于检测有效数据和噪声
下面是同步模式需要的引脚 - SCLK :产生数据传输的时钟信号,对应于主机模式下的SPI接口线
下面是硬件流控需要的引脚 - nCTS: Clear To Send。当引脚为高的时候,在最近一次传输完成之后不再进行数据发送
- nRTS: Request to send。引脚为低的时候表明准许接收数据
流控的用途举例:比如电脑发送给单片机的数据,而这些数据比较重要,不允许有大量的丢失,但是电脑的速度比较快,发送给单片机的数据单片机可能没有来得及处理完毕,就有新的数据发送过来,这样就可能会造成数据缓冲区溢出,这个时候可以向电脑端发送数据停止标志来让电脑停止发送数据一段时间,等到数据处理完毕之后再重新进行发送。而现在普遍使用软件流控,也就是通过向发送端发送XON与XOFF标志来实现,XON就是16进制的ctrl+q组合键对应的键码,XOFF是16进制的ctrl+s对应的组合键键码
4、数据传输
数据格式
有8bit与9bit数据格式,每种数据格式都是由逻辑’0’作为起始位,以逻辑’1’作为结束位。break frame与idle frame有少许不同,具体如下图:
传输过程
- 当传输使能引脚 (TE) 被设置之后, 在移位寄存器中的数据就会被输出到 TX 引脚上,并且对应的时钟信号被输出到 SCLK 脚上面
- 数据由低电平’0’位开始,被停止位标志结束。STM32F407ZG的停止位包括0.5,1,1.5,2bit
配置停止位
1 stop bit : 默认的停止位配置
2 Stop bits : USART, 单线和 modem 模式均支持该停类型停止位
0.5 stop bit : 主要用于 Smartcard 模式下接收数据
1.5 stop bits : 用于 Smartcard 模式下接收与发送数据
\\# 一个 idle 帧的传输必须包含停止位 #\\
停止位:
数据传输步骤
- 使能 USART。通过写 USART_CR1 寄存器的 UE 位为 1
- 写 USART_CR1 寄存器的 M 位来定义数据长度,8位或者9位
- 通过写 USART_CR2 寄存器来设置停止位
- 如果是多缓冲区传输的话,通过写 USART_CR3 寄存器来使能 DMA 传输。配置 DMA 寄存器设置为多缓冲区传输
- 通过 USART_BRR 设置波特率
- 初次传输的时候设置 USART_CR1 寄存器的 TE 位来发送一个 idle 帧
- 把需要传输的数据写到 USART_DR 寄存器(同时该操作会清除 TXE 位)。在单缓冲区传输的时候应该每次都重复进行此动作来进行数据的传输
- 在写入最后一个传输数据到 USART_DR 寄存器值后,等待 TC 信号,直到 TC=1,此时表明最后一帧传输完毕。
- 以下情况下TC被清除
- 对 USART_SR 寄存器的读操作
- 对 USART_DR 寄存器的写操作
下图是TC与TXE在传输过程中的状态变化
数据接收步骤
- 通过写 USART_CR1 寄存器的 UE 位为 1 来使能 USART
- 写 USART_CR1 寄存器的 M 位来定义数据长度,8位或者9位
- 通过写 USART_CR2 寄存器来设置停止位
- 如果是多缓冲区传输的话,通过写 USART_CR3 寄存器来使能 DMA 传输。配置 DMA 寄存器设置为多缓冲区传输
- 通过 USART_BRR 设置波特率
- 设置 USART_CR1 寄存器的 RE 位。这个操作会使能数据接收的接收
当接收到一个数据的时候RXNE 位被设置。表明移位寄存器中的数据被传输到了 RDR。也就是说数据已经可以被读出了
• 如果 RXNEIE 被设置的话,系统就会产生一个中断
• 如果有帧错误的话,错误标志就会被设置
• 在多缓冲区模式下, RXNE 在每次接收完1byte数据之后就会被置位并且被 DMA 的读数据寄存器操作清除置位
• 在单缓冲区模式下, 对 USART_DR 寄存器的读操作会清除 RXNE 标志。该位也可以通过写0来进行清除。并且在下一次传输之前 RXNE 位必须被清除,以防止数据被覆盖
串口功能表:
5、波特率
波特率计算
当 OVER8=0时, DIV_fraction[3:0] 可以被读写
当 OVER8=1时, DIV_fraction[2:0] 可以被读写,并且 DIV_fraction[3] 必须被清零
波特率计算公式:
PS:下面的0d27指的是10进制的27,也就是我们常说的自然数27,其他的0dxx也是类似理解
OVER8 = 0时的例子:
If DIV_Mantissa = 0d27 and DIV_Fraction = 0d12 (USART_BRR = 0x1BC), then
Mantissa (USARTDIV) = 0d27
Fraction (USARTDIV) = 12/16 = 0d0.75
Therefore USARTDIV = 0d27.75
To program USARTDIV = 0d25.62
This leads to:
DIV_Fraction = 16*0d0.62 = 0d9.92
The nearest real number is 0d10 = 0xA
DIV_Mantissa = mantissa (0d25.620) = 0d25 = 0x19
Then, USART_BRR = 0x19A hence USARTDIV = 0d25.625
To program USARTDIV = 0d50.99
This leads to:
DIV_Fraction = 16*0d0.99 = 0d15.84
The nearest real number is 0d16 = 0x10 => overflow of DIV_frac[3:0] => carry must be
added up to the mantissa
DIV_Mantissa = mantissa (0d50.990 + carry) = 0d51 = 0x33
Then, USART_BRR = 0x330 hence USARTDIV = 0d51.000
假如我们想要在时钟为84M频率下设置波特率为115200,并且OVER8 = 0,由上面的波特率计算公式以及USARTDIV计算公式可知:
- 首先算出USARTDIV需要被设置为45.57
- 拆分整数部分与小数部分,分别是45与0.57
- 整数部分由 USART_BRR 的 DIV_Mantissa 决定,这个自然不必说,而小数部分由 DIV_Fraction 决定,可以知道 DIV_Fraction 的值需要是 0.57 * 16 = 9.12,最接近9.12的是9,所以需要将 DIV_Fraction 的值设置为9
OVER8 = 1时的例子:
If DIV_Mantissa = 0x27 and DIV_Fraction[2:0]= 0d6 (USART_BRR = 0x1B6), then
Mantissa (USARTDIV) = 0d27
Fraction (USARTDIV) = 6/8 = 0d0.75
Therefore USARTDIV = 0d27.75
To program USARTDIV = 0d25.62
This leads to:
DIV_Fraction = 8*0d0.62 = 0d4.96
The nearest real number is 0d5 = 0x5
DIV_Mantissa = mantissa (0d25.620) = 0d25 = 0x19
Then, USART_BRR = 0x195 => USARTDIV = 0d25.625
To program USARTDIV = 0d50.99
This leads to:
DIV_Fraction = 8*0d0.99 = 0d7.92
The nearest real number is 0d8 = 0x8 => overflow of the DIV_frac[2:0] => carry must be
added up to the mantissa
DIV_Mantissa = mantissa (0d50.990 + carry) = 0d51 = 0x33
Then, USART_BRR = 0x0330 => USARTDIV = 0d51.000
下面是不同的PCLK频率下对应波特率的关系。我们由时钟那一节可以知道,USART1与USART6是属于APB2域,也就是PCLK2域,此域手册已经说明了最大是84M,而其余的串口是在APB1域,也就是PCLK1域,该域最大时钟频率为42M
接收器能够接受的时钟偏差
在异步接收模式下,只有系统时钟偏差小于接收器能够接受的最大偏差值的时候接收器才能够正确接收数据:
- DTRA: 由于发送器误差引起的偏差 (包括发送器晶振的误差)
- DQUANT: 接收器波特率引起的误差
- DREC: 接收器晶振引起的误差
- DTCL: 传输线路引起的误差
- 满足以下公式给出的条件的时候才能够正常的工作
- DTRA + DQUANT + DREC + DTCL < USART receiver’s tolerance
- DTRA + DQUANT + DREC + DTCL < USART receiver’s tolerance
接收器最大忍受偏差值取决于
- 10 or 11 bit 数据长度选择
- 8 or 16 过采样选择
- 是否使用分段波特率。(也就是带小数位的波特率计算)
- 使用 1 bit or 3 bits 对数据进行采样
误差耐受值
6、实例
在设置任何模块的时候,都要先使能它的时钟,然后才能够设置它的寄存器,否则寄存器设置不成功
/* 初始化usart1
* 波特率:115200
* 模块时钟:84M
*/
void bym_uart_init(void)
{
BYM_RCC_struct->APB2ENR |= (1 << 4); //Enable usart1's clock
// WRITE_R_BIT_BAND(0x23844, 4, 1); //位带操作,同上一个语句效果一样,该宏定义在 STM32内存与总线一节有提到
BYM_USART_struct->BRR &= ~((0xfff << 4) | (0xf < 0)); //Clear DIV_Mantissa and DIV_Fraction
BYM_USART1_struct->BRR |= ((45 << 4) | (9 << 0)); //DIV_Mantissa = 45, DIV_Fraction = 9 USARTDIV = 45.57
/* oversampling by 16 USART enabled 1 Start bit, 8 Data bits, n Stop bit
* Parity control disabled All interrupts are disiabled
* BYM_USART_struct->CR1
*/
BYM_USART1_struct->CR1 &= ~(1 << 15); //oversampling by 16
BYM_USART1_struct->CR1 |= (1 << 3); //TX enabled
BYM_USART1_struct->CR1 |= (1 << 13); //USART enabled
/* 1 stop bit SCLK pin disabled
* BYM_USART_struct->CR2
*/
/* No CTS/RTS SCLK pin disabled
* BYM_USART_struct->CR3
*/
/* 引脚复用为usart功能,串口1对应的是PA9与PA10引脚 */
gpio_init(BYM_GPIOA, BYM_Px9, BYM_PULL_UP, BYM_GPALT, BYM_LOW_LEVEL, BYM_PUSH_PULL);
gpio_set_alt_speed(BYM_GPIOA, BYM_Px9, BYM_AF7, BYM_GPIO_HIGI_SPEED);
gpio_init(BYM_GPIOA, BYM_Px10, BYM_PULL_UP, BYM_GPALT, BYM_LOW_LEVEL, BYM_PUSH_PULL);
gpio_set_alt_speed(BYM_GPIOA, BYM_Px10, BYM_AF7, BYM_GPIO_HIGI_SPEED);
}
该初始化函数并不支持动态的指定时钟频率与串口波特率,但是根据上面的公式可以实现一个动态的设定时钟频率与串口波特率的串口初始化函数。更改部分代码如下:
void bym_uart_init(u32 clk, u32 baud)
{
u32 usart_div = 0;
BYM_RCC_struct->APB2ENR |= (1 << 4); //Enable usart1's clock
usart_div = ((clk * 1000000 / 16) * 100) / baud;
usart_div = (usart_div / 100) * 100 + (usart_div % 100) * 16 / 100;
// BYM_USART_struct->BRR &= ~((0xfff << 4) | (0xf < 0)); //Clear DIV_Mantissa and DIV_Fraction
BYM_USART1_struct->BRR |= (((usart_div / 100) << 4) | (usart_div % 100)); //DIV_Mantissa = 45, DIV_Fraction = 9 USARTDIV = 45.57
... ...
}
添加对printf函数的支持,添加以下代码就可以使用printf函数通过串口0进行数据打印了
#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 typedefed in stdio.h. */
FILE __stdout;
/* 定义_sys_exit()避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
/* 重新定义fputc函数 */
int fputc(int ch, FILE *f)
{
while(!(BYM_USART1_struct->SR & (1 << 6)));
BYM_USART1_struct->DR = ch;
return ch;
}
7、纠正编程风格之尽量使用面向对象操作
结构体定义
struct bym_usart_init_structure
{
u32 baud;
u32 pclk;
u8 oversample;
u8 tx_enable;
u8 rx_enable;
};
驱动函数
void bym_uart_init(struct bym_usart_init_structure *usart_struct)
{
u32 usart_div = 0;
BYM_RCC_struct->APB2ENR |= (1 << 4); //Enable usart1's clock
// WRITE_R_BIT_BAND(0x23844, 4) = 1;
usart_div = ((usart_struct->pclk * 1000000 / 16) * 100) / usart_struct->baud;
usart_div = (usart_div / 100) * 100 + (usart_div % 100) * 16 / 100;
// BYM_USART_struct->BRR &= ~((0xfff << 4) | (0xf < 0)); //Clear DIV_Mantissa and DIV_Fraction
BYM_USART1_struct->BRR |= (((usart_div / 100) << 4) | (usart_div % 100)); //DIV_Mantissa , DIV_Fraction , USARTDIV
/* oversampling by 16 USART enabled 1 Start bit, 8 Data bits, n Stop bit
* Parity control disabled All interrupts are disiabled
* BYM_USART_struct->CR1
*/
BYM_USART1_struct->CR1 &= ~(1 << 15); //oversampling by 16
BYM_USART1_struct->CR1 |= (usart_struct->oversample << 15);
BYM_USART1_struct->CR1 &= ~(1 << 3);
BYM_USART1_struct->CR1 |= (usart_struct->tx_enable << 3); //TX set
BYM_USART1_struct->CR1 &= ~(1 << 2);
BYM_USART1_struct->CR1 |= (usart_struct->rx_enable << 2); //RX set
BYM_USART1_struct->CR1 |= (1 << 13); //USART enabled
/* 1 stop bit SCLK pin disabled
* BYM_USART_struct->CR2
*/
/* No CTS/RTS SCLK pin disabled
* BYM_USART_struct->CR3
*/
/* 引脚复用为usart功能,串口1对应的是PA9与PA10引脚 */
gpio_init(BYM_GPIOA, BYM_Px9, BYM_PULL_UP, BYM_GPALT, BYM_LOW_LEVEL, BYM_PUSH_PULL);
gpio_set_alt_speed(BYM_GPIOA, BYM_Px9, BYM_AF7, BYM_GPIO_HIGI_SPEED);
gpio_init(BYM_GPIOA, BYM_Px10, BYM_PULL_UP, BYM_GPALT, BYM_LOW_LEVEL, BYM_PUSH_PULL);
gpio_set_alt_speed(BYM_GPIOA, BYM_Px10, BYM_AF7, BYM_GPIO_HIGI_SPEED);
}
应用程序
struct bym_usart_init_structure usart_struct;
usart_struct.baud = 115200; //baud
usart_struct.oversample = 0;
usart_struct.pclk = 84;
usart_struct.rx_enable = 0; //Disable
usart_struct.tx_enable = 1; //Enable
bym_uart_init(&usart_struct);
上面只是实现了一个及其简单的面向对象的操作,在需要初始化一个串口的时候,只需要申请一个 bym_usart_init_structure 类型的结构体,填充结构体里面的数据,然后将结构体转交给 bym_uart_init 执行串口的初始化,并不需要过多的关注内部结构,并且又能够很轻松的实现自定义参数的初始化。这种操作在linux内核里面非常常见,基本上写驱动都是采用这样的面向对象风格的编程,以后的代码风格尽量就会采用这种风格进行编写。由于时间比较仓促, 本模块并没有很好的实现面向对象,函数的容错性很小,并且不支持对多个串口使用同一个函数进行初始化,还有停止位,校验位等等都不能够实现自定义,该函数功能太过弱小,以待改进。
PS:一个使用的调试小技巧,使用printf(“%s %s %d”, __FILE__, __FUNCTION__, __LINE__); 语句进行打印,该语句中的__FILE__, __FUNCTION__, __LINE__在编译的时候均会被替换成为常数,该语句会打印该语句所在的详细路径,包括所在文件名、函数名、行数
以上是关于STM32之usart的主要内容,如果未能解决你的问题,请参考以下文章