十、串口打印信息
1. 配置流程
一般我们使用串口,都需要有以下几个步骤。
- 关闭寄存器保护
- 配置GPIO复用模式
- 开启串口时钟
- 配置串口(配置一些参数)
- 配置中断
- 使能串口功能
1.1 关闭寄存器保护
HC32的很多寄存器不可以直接进行配置,需要先解锁保护寄存器,才可以配置。
我们先定义解锁的一些寄存器:
#define LL_PERIPH_SEL (LL_PERIPH_GPIO | LL_PERIPH_FCG | LL_PERIPH_PWC_CLK_RMU | \
LL_PERIPH_EFM | LL_PERIPH_SRAM)
2
然后调用函数进行解锁,这个函数是库函数封装好的:
// 关闭寄存器外设写保护
LL_PERIPH_WE(LL_PERIPH_SEL);;
2
1.2 配置GPIO复用模式
HC32的引脚是可以有复用功能的,就是说单个引脚可有很多个功能,默认的功能一般都是作为GPIO使用。在hc32_ll_gpio.h中可以查找到设置复用的函数
void GPIO_SetFunc(uint8_t u8Port, uint16_t u16Pin, uint16_t u16Func);
这个函数有三个参数,第一个参数就是要配置的引脚端口,第二个参数是要配置的引脚,第三个参数就是要复用的功能。关于最后一个Func参数可以到数据手册的第43页进行查找,如图1-2-1所示。
从图1-2-1可以看到PA9对应的USART1_TX的功能复用为AF7,PA10对应的USART1_RX的功能复用为Func20,在库函数中官方已经封装了Func所以我们使用相应的参数:GPIO_FUNC_20即可,代码编写如下。
调用复用函数使能复用功能。
//IO口用作串口引脚要配置复用模式
GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_09, GPIO_FUNC_20);
GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_10, GPIO_FUNC_20);
2
3
通过上面两句就可以把PA9,PA10设置为串口功能了。
1.3 开启串口时钟
想要知道时钟对应的函数名称,我们打开数据手册第187页,在寄存器说明一章节中找到USART相关的函数:
我们在FCG3里面找到了USART相关的功能名称,所以我们就可以知道此次时钟是 FCG3 !如下图:
那使能时钟的函数 FCG_Fcg3PeriphClockCmd ,只需要传入对应的参数即可。使能端口A的时钟就把GPIOA的时钟当做参数传入。第二步就是开启串口的时钟,把对应的串口1的时钟 FCG3_PERIPH_USART1 传入即可。如下:
// 使能USART时钟
FCG_Fcg3PeriphClockCmd(FCG3_PERIPH_USART1, ENABLE);
2
1.4 配置串口
我们依然使用结构体的方式进行配置串口数据,里面有些参数是必须要配置的:
- 波特率:我们使用形参__Baud可以自由调整波特率
- 字节长度:8位
- 停止位:1位停止位
- 校验位:不需要校验位
- 收发模式:收发
- 流控选择:不流控 相关的代码:
stc_usart_uart_init_t stcUartInit;
(void)USART_UART_StructInit(&stcUartInit);
stcUartInit.u32ClockSrc = USART_CLK_SRC_INTERNCLK; // 选择内部时钟源
stcUartInit.u32ClockDiv = USART_CLK_DIV1; // 选择不分频
stcUartInit.u32CKOutput = USART_CK_OUTPUT_DISABLE; // 不输出时钟
stcUartInit.u32Baudrate = __Baud; // 波特率
stcUartInit.u32DataWidth = USART_DATA_WIDTH_8BIT; // 数据宽度8位
stcUartInit.u32StopBit = USART_STOPBIT_1BIT; // 停止位1位
stcUartInit.u32OverSampleBit = USART_OVER_SAMPLE_8BIT; // 采样点数为8位
stcUartInit.u32FirstBit = USART_FIRST_BIT_LSB; // 低位优先
stcUartInit.u32StartBitPolarity = USART_START_BIT_FALLING; // 起始位为下降沿
stcUartInit.u32HWFlowControl = USART_HW_FLOWCTRL_NONE; // 硬件流控为无
// 初始化USART
USART_UART_Init(CM_USART1, &stcUartInit, NULL);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
首先声明了一个名为 stcUartInit
的结构体变量,类型为 stc_usart_uart_init_t
。 接着对 stcUartInit
结构体的各个成员进行了赋值:
u32ClockSrc
被赋值为USART_CLK_SRC_INTERNCLK
,表示选择内部时钟源。u32ClockDiv
被赋值为USART_CLK_DIV1
,表示选择不分频。u32CKOutput
被赋值为USART_CK_OUTPUT_DISABLE
,表示不输出时钟。u32Baudrate
被赋值为__Baud
,表示波特率的设定值。u32DataWidth
被赋值为USART_DATA_WIDTH_8BIT
,表示数据宽度为8位。u32StopBit
被赋值为USART_STOPBIT_1BIT
,表示停止位为1位。u32OverSampleBit
被赋值为USART_OVER_SAMPLE_8BIT
,表示采样点数为8位。u32FirstBit
被赋值为USART_FIRST_BIT_LSB
,表示低位优先。u32StartBitPolarity
被赋值为USART_START_BIT_FALLING
,表示起始位为下降沿。u32HWFlowControl
被赋值为USART_HW_FLOWCTRL_NONE
,表示硬件流控为无。
最后一行代码调用了USART_UART_Init
函数,初始化了USART
模块,传入了CM_USART1
作为第一个参数,&stcUartInit
作为第二个参数,表示使用之前设置好的初始化参数,最后一个参数为NULL
,表示没有使用中断。
1.5 配置中断
我们需要配置接收中断和错误中断,这样才能够让串口工作的效果最好。
注意在.s的启动文件中,没有像别的芯片那样对功能和中断绑死操作,而是直接给了很多的中断请求,可以自由绑定(展示部分):
// IRQ & NVIC初始化
stc_irq_signin_config_t stcIrqSigninCfg;
// IRQ & NVIC配置
stcIrqSigninCfg.enIRQn = INT000_IRQn; // 错误中断
stcIrqSigninCfg.enIntSrc = INT_SRC_USART1_EI; //
stcIrqSigninCfg.pfnCallback = &USART1_ERROR_IRQHandler; // 中断回调函数
(void)INTC_IrqSignIn(&stcIrqSigninCfg);
NVIC_ClearPendingIRQ(stcIrqSigninCfg.enIRQn);
NVIC_SetPriority(stcIrqSigninCfg.enIRQn, DDL_IRQ_PRIO_DEFAULT); // 优先级
NVIC_EnableIRQ(stcIrqSigninCfg.enIRQn);
// IRQ & NVIC配置
stcIrqSigninCfg.enIRQn = INT001_IRQn; // 接收中断
stcIrqSigninCfg.enIntSrc = INT_SRC_USART1_RI; //
stcIrqSigninCfg.pfnCallback = &USART1_RECV_IRQHandler; // 中断回调函数
(void)INTC_IrqSignIn(&stcIrqSigninCfg);
NVIC_ClearPendingIRQ(stcIrqSigninCfg.enIRQn);
NVIC_SetPriority(stcIrqSigninCfg.enIRQn, DDL_IRQ_PRIO_00); // 优先级
NVIC_EnableIRQ(stcIrqSigninCfg.enIRQn);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这段代码是用于初始化中断请求 (IRQ) 和嵌套向量中断控制器 (NVIC)。
首先,声明了一个名为 stcIrqSigninCfg 的结构体变量,类型为 stc_irq_signin_config_t。
然后进行了两次 IRQ & NVIC 配置:
- 针对错误中断 (INT000_IRQn) 的配置
enIRQn
被设置为INT000_IRQn
表示错误中断。enIntSrc
被设置为INT_SRC_USART1_EI
表示中断源为USART1
的错误中断。pfnCallback
被设置为&USART1_ERROR_IRQHandler
,表示中断回调函数为USART1_ERROR_IRQHandler
。- 调用
INTC_IrqSignIn
函数将中断注册到中断控制器。 - 使用
NVIC_ClearPendingIRQ
清除该中断的挂起状态。 - 使用
NVIC_SetPriority
设置该中断的优先级为默认值DDL_IRQ_PRIO_DEFAULT
。 - 最后使用
NVIC_EnableIRQ
使能该中断。
- 针对接收中断 (INT001_IRQn) 的配置
enIRQn
被设置为INT001_IRQn
表示接收中断。enIntSrc
被设置为INT_SRC_USART1_RI
表示中断源为USART1
的接收中断。pfnCallback
被设置为&USART1_RECV_IRQHandler
,表示中断回调函数为USART1_RECV_IRQHandler
。- 调用
INTC_IrqSignIn
函数将中断注册到中断控制器。 - 使用
NVIC_ClearPendingIRQ
清除该中断的挂起状态。 - 使用
NVIC_SetPriority
设置该中断的优先级为DDL_IRQ_PRIO_00
。 - 最后使用
NVIC_EnableIRQ
使能该中断。
我们在初始化化的时候定义了两个函数:
- USART1_ERROR_IRQHandler:错误中断处理函数。
- USART1_RECV_IRQHandler:接收中断处理函数。 我们直接将两个函数实现:
/******** 串口 错误中断服务函数 ***********/
void USART1_ERROR_IRQHandler(void)
{
(void)USART_ReadData(CM_USART1);
USART_ClearStatus(CM_USART1, (USART_FLAG_PARITY_ERR | USART_FLAG_FRAME_ERR | USART_FLAG_OVERRUN));
}
/******** 串口 接收中断服务函数 ***********/
void USART1_RECV_IRQHandler(void)
{
u1_recv_buff[] = USART_ReadData(CM_USART1); // 把接收到的数据放到缓冲区中
while( SET == USART_GetStatus(CM_USART1, USART_FLAG_RX_FULL) ){} // 等待数据接收完成
u1_recv_buff[] = '\0';
u1_recv_flag = 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.6 使能串口功能
接下来我们就是对TX和RX功能进行使能,并将接收中断使能。
// 开启 TX 功能
USART_FuncCmd(CM_USART1, USART_TX, ENABLE);
// 开启 RX 功能
USART_FuncCmd(CM_USART1, USART_RX, ENABLE);
// 开启 接收 中断
USART_FuncCmd(CM_USART1, USART_INT_RX, ENABLE);
2
3
4
5
6
7
8
2. 串口发送数据
配置好串口之后,下一步的操作就是要发送数据。
void USART_WriteData(CM_USART_TypeDef *USARTx, uint16_t u16Data)
USARTx
:指向 USART 实例寄存器基址的指针。作为参数传入时,可以使用CM_USARTx
中的一个值。u16Data
:表示要发送的数据,其类型为uint16_t
。- 无返回值
en_flag_status_t USART_GetStatus(const CM_USART_TypeDef *USARTx, uint32_t u32Flag) 这个函数是获取状态寄存器的标志。
USARTx
:指向 USART 实例寄存器基址的指针。作为参数传入时,可以使用CM_USARTx
中的一个值。u32Flag
:表示 USART 的标志位类型,可以是USART_Flag
宏组合值之一。 状态位选项为图2-1-1所示。
从图2-1-2可以了解到当将要发送的数据写入USART_DATA时,此位被清0,当数据发送完成之后,此位置1。所以当检测到此位为1时就表明当前数据缓冲区为空,可以继续发送数据。 关于串口发送数据可以封装为一个函数如下。
void usart_send_data(uint8_t ucch)
{
USART_SendData(BSP_USART, (uint8_t)ucch);
// 等待发送数据缓冲区标志置位
while( RESET == USART_GetFlagStatus(BSP_USART, USART_FLAG_TXE) ){}
}
2
3
4
5
6
7
当我们调用
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,如图2-1-3所示。
这样一个字符一个字符的打印输出是不是很麻烦,没关系,我们可以再进行封装一层,一次发送一个字符串。
void usart_send_String(uint8_t *ucstr)
{
while(ucstr && *ucstr) // 地址为空或者值为空跳出
{
usart_send_data(*ucstr++);
}
}
2
3
4
5
6
7
通过调用
usart_send_String("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_WriteData(CM_USART1, ch);
while( RESET == USART_GetStatus(CM_USART1, USART_FLAG_TX_EMPTY) ){}
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
WARNING
📌 注意:从第1行到第18行的代码,会在我们不使用微库的时候生效,我们这样设置就能正常使用printf函数了,而且兼容性会更高。
编写好fputc函数之后就可以使用printf函数输出信息了。
4. 实验现象
烧写我们的代码之后,打开串口调试助手,每隔1s会打印一次信息,信息分别为整数每隔1s+1和小数每隔1s+0.11,打印信息如图4-1-1所示。