十、串口打印信息
1. 配置流程
一般我们使用串口,都需要有以下几个步骤。
- 开启时钟(包括串口时钟和GPIO时钟)
- 配置GPIO复用模式
- 配置GPIO的模式
- 配置GPIO的输出
- 配置串口(配置一些参数)
- 使能串口(串口使能和发送使能)
1.1 开启时钟
使用串口1的话就是PA8和PA9引脚。那第一步就是先开启端口A的时钟,在库函数点灯那一章节给大家介绍了使能时钟的函数RCC_AHBPeriphClk_Enable,只需要传入对应的参数即可。使能端口A的时钟就把GPIOA的时钟当做参数传入。
第二步就是开启串口的时钟,使用RCC_APBPeriphClk_Enable2函数把对应的参数传入。
//配置RCC
RCC_AHBPeriphClk_Enable(RCC_AHB_PERIPH_GPIOA, ENABLE); // 使能GPIO时钟
RCC_APBPeriphClk_Enable2(RCC_APB2_PERIPH_UART1, ENABLE); // 使能串口时钟
2
3
1.2 配置GPIO
配置GPIO的模式还是使用GPIO_Init这个函数,转化为代码如下。
// 配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
//UART TX RX 复用
PA08_AFx_UART1TXD();
PA09_AFx_UART1RXD();
GPIO_InitStructure.Pins = GPIO_PIN_8; // 引脚
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStructure.Speed = GPIO_SPEED_HIGH; // 输出速度高
GPIO_Init(CW_GPIOA, &GPIO_InitStructure); // 初始化GPIO
GPIO_InitStructure.Pins = GPIO_PIN_9; // 引脚
GPIO_InitStructure.Mode = GPIO_MODE_INPUT_PULLUP; // 上拉输入
GPIO_Init(CW_GPIOA, &GPIO_InitStructure); // 初始化GPIO
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.3 配置串口
我们依然使用结构体的方式进行配置串口数据,里面有些参数是必须要配置的:
- 波特率:我们使用形参__rate可以自由调整波特率
- 字节长度:8位
- 停止位:1位停止位
- 校验位:不需要校验位
- 收发模式:收发
- 流控选择:不流控
相关的代码:
// 配置UART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = __rate; // 波特率
USART_InitStructure.USART_Over = USART_Over_16; // 配置USART的过采样率。
USART_InitStructure.USART_Source = USART_Source_PCLK; // 设置时钟源
USART_InitStructure.USART_UclkFreq = 64000000; //设置USART时钟频率(和主频一致即可)
USART_InitStructure.USART_StartBit = USART_StartBit_FE; //RXD下降沿开始
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 停止位1
USART_InitStructure.USART_Parity = USART_Parity_No ; // 不使用校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用流控
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
USART_Init(CW_UART1, &USART_InitStructure); // 初始化串口1
2
3
4
5
6
7
8
9
10
11
12
13
1.4 配置中断
我们并不知道串口什么时候会发回来消息,轮询等待是不现实的,所以我们需要中断来帮我们监控,如果有数据发来,则进入中断进行处理。 转化为代码为:
//优先级,无优先级分组
NVIC_SetPriority(UART1_IRQn, 0);
//UARTx中断使能
NVIC_EnableIRQ(UART1_IRQn);
//使能UARTx RC中断
USART_ITConfig(CW_UART1, USART_IT_RC, ENABLE);
2
3
4
5
6
7
1.5 编写串口中断服务函数
void UART1_IRQHandler(void)
{
uint8_t TxRxBuffer;
if (USART_GetITStatus(CW_UART1, USART_IT_RC) != RESET)
{
// 接收一个字节
TxRxBuffer = USART_ReceiveData_8bit(CW_UART1);
// 剩下的处理逻辑........
// 清除标志位
USART_ClearITPendingBit(CW_UART1, USART_IT_RC);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 中断入口:函数开始直接进入中断处理逻辑。
- 中断状态检查:通过USART_GetITStatus(CW_UART1, USART_IT_RC)检查是否是因为接收中断(USART_IT_RC表示接收中断)触发了中断。如果不是接收中断,则不执行后续操作。
- 数据接收:如果接收中断发生,通过USART_ReceiveData_8bit(CW_UART1)读取接收到的一个字节数据,并将其存储在局部变量TxRxBuffer中。
- 在这个部分就可以编写其他的处理。
- 清除中断标志:最后,通过USART_ClearITPendingBit(CW_UART1, USART_IT_RC)清除接收中断标志位,这是非常重要的一步,以避免下次同类型中断发生前,中断标志一直保持激活状态,造成重复中断处理。
到此,串口使能就可以使用了。
2. 串口发送数据
配置好串口之后,下一步的操作就是要发送数据。
void USART_SendData_8bit(UART_TypeDef *USARTx, uint8_t Data)
函数作用:通过USARTx发送一个数据(8bit) 参数:
- USARTx :USARTx外设,例如CW_UART1、CW_UART2和CW_UART3
- Data :待发送的数据
我们还需要一个函数用来判断串口发送的状态。
FlagStatus USART_GetFlagStatus(UART_TypeDef *USARTx, uint16_t USART_FLAG)
函数作用:获取USARTx标志位
参数:
- USARTx :USARTx外设 可以是 CW_UART1、CW_UART2、CW_UART3
- USART_FLAG :标志
返回值:SET 或者 RESET(1或0)
从上图可以了解到当将要发送的数据写入USART_DATA时,此位被清0,当数据发送完成之后,此位置1。所以当检测到此位为1时就表明当前数据缓冲区为空,可以继续发送数据。
关于串口发送数据可以封装为一个函数如下。
/******************************************************************
* 函 数 名 称:usart_send_data
* 函 数 说 明:发送一个字节的数据
* 函 数 形 参:ucch:一个字节的数据
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void usart_send_data(uint8_t ucch)
{
// 发送一个字节
USART_SendData_8bit(CW_UART1, (uint8_t)ucch);
// 等待发送完成
while( RESET == USART_GetFlagStatus(CW_UART1, USART_FLAG_TXE) ){}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当我们调用以下函数并且下载时
usart_send_data('h');
usart_send_data('e');
usart_send_data('l');
usart_send_data('l');
usart_send_data('o');
2
3
4
5
将会在串口助手打印hello,如下图所示。
这样一个字符一个字符的打印输出是不是很麻烦?没关系,我们可以再进行封装一层,一次发送一个字符串。
void usart_send_String(uint8_t *ucstr)
{
while(ucstr && *ucstr) // 地址为空或者值为空跳出
{
usart_send_data(*ucstr++);
}
}
2
3
4
5
6
7
通过调用
usart_send_String((uint8_t *)"hello\r\n");
这一句就可以打印出hello。
3. 串口重定向
上面封装的发送字符串的方式打印信息看似方便,但如果我们想要打印数字,小数,该怎么打印呢?大家是否习惯使用了printf这个函数,可以通过%d,%f打印整形和小数,这一小节就教大家怎么把串口重定向到printf函数。
3.1 串口重定向介绍
C语言中的printf函数默认输出设备是显示器,如果要在串口显示,必须重新定义标准库函数里调用的与输出设备相关的函数。需要注意的是,在keil中使用printf一定要勾选“微库”选项。
3.2 printf重定向
首先c语言的printf函数中不断循环调用fputc函数,所以需要重写fputc函数,这个函数的功能就是打印输出一个字符,这不正和我们编写的usart_send_data函数功能一样。fputc函数可写为
#if !defined(__MICROLIB)
//不使用微库的话就需要添加下面的函数
#if (__ARMCLIB_VERSION <= 6000000)
//如果编译器是AC5 就定义下面这个结构体
struct __FILE
{
int handle;
};
#endif
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
#endif
/* retarget the C library printf function to the USART */
int fputc(int ch, FILE *f)
{
// 发送一个字节
USART_SendData_8bit(CW_UART1, (uint8_t)ch);
// 等待发送完成
while( RESET == USART_GetFlagStatus(CW_UART1, USART_FLAG_TXE) ){}
return ch;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
注意
从第1行到第18行的代码,会在我们不使用微库的时候生效,我们这样设置就能正常使用printf函数了,而且兼容性会更高。
编写好fputc函数之后就可以使用printf函数输出信息了。
printf("hello\r\n");
4. 实验现象
关于这一章节的代码,在立创·地文星CW32F030C8T6开发板资料/第03章软件资料/代码例程/004串口打印信息。
烧写我们的代码之后,打开串口调试助手,每隔1s会打印一次信息,信息分别为整数每隔1s+1和小数每隔1s+0.11,打印信息如下图所示。