1.本文所需的所有设备:
一片STM32F103C8T6,一个USB-TTL的串口,一个ST-Link烧录器,以及基本的开发工具
本文所需文件如下:
1.
2.
3.
文件里面就是我的实例代码
2.基本串口开启
首先,请参照上一篇文章完成初始化设置,以及IO口的开启
针对之前的引脚定义图,我们看到我们定义A9,A10分别为TX,RX。根据定义,我们连好对应的串口连线
修改主程序如下所示:
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_UART_Transmit(&huart1, (uint8_t *)"Hello, World!\r\n", 15, 1000);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(1000);
}
/* USER CODE END 3 */
此时我们定义了串口将2s发送一个 Hello, World。编译,烧录后打开串口助手后就能看到结果如下
这就证明我们的串口发送消息基本已经没问题了
3.定义printf,并定向到串口1
首先添加头文件到如下位置
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
再将如下函数程序添加到初始化程序中(while主程序前)
/* USER CODE BEGIN 0 */
// 开启printf的使用,定位到串口1
int fputc(int ch, FILE *f) // printf
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
int fgetc(FILE *f) // getchar
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
/* USER CODE END 0 */
修改主程序如下所示
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// HAL_UART_Transmit(&huart1, (uint8_t *)"Hello, World!\r\n", 15, 1000);
printf("printf Hello, World!\r\n");
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(1000);
}
/* USER CODE END 3 */
烧录,编译后看到串口结果如下所示,说明printf已经可以正常使用了
注意:在调试STM32的程序时,printf是一个很常用的函数,尤其是在调节PID的参数时,因为他可以帮助我们很方便的将多个浮点数通过串口打出来。但是这里需要注意,事实上,printf并不是一个简单的函数,他其实会占用不少的CPU资源。因此,尽量不要使用printf打印太多的浮点数,尽量不超过8个,最好限制在6个;同时打印浮点数时也最好限制浮点数的位数到2位。否则就可能遇到CPU崩溃的情况,这也是我曾遇到过的惨痛教训。
4.串口接收消息
首先,通过串口接收消息其实也可以不使用串口中断,可以将接受消息的步骤放在主程序中,通过主程序循环读取消息,接着再处理串口所接收到的消息。这样你也可以不断地接受串口消息,但又两个问题:
1.接受消息并不是瞬间的,不是以中断的形式发生的,是以主程序的频率发生的;
2.主程序内不能有任何延时的程序,这里主要是针对的主程序的频率考虑的。
因此,我们可以采用滴答时钟的办法来代替延时,具体的办法如下所示:
/* USER CODE BEGIN WHILE */
int tick_time = 0;
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
tick_time++;
// chelk the time
// printf("tick_time: %d\r\n", tick_time);
if (tick_time % 200 == 0)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
}
if (tick_time % 200 == 100)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
}
// open the uart1 reciver
HAL_UART_Receive(&huart1, receive_Buff1, sizeof(receive_Buff1), 10);
// analyse the receive_buff
if (receive_Buff1[0] != 0)
{
if ((receive_Buff1[0] == 'O') && (receive_Buff1[1] == 'K'))
{
printf("RIGHT\r\n");
}
if ((receive_Buff1[0] == 'N') && (receive_Buff1[1] == 'O'))
{
printf("ERROR\r\n");
}
}
}
/* USER CODE END 3 */
此时可以观察到生命指示灯依旧在闪烁(图片略),同时通过串口发送消息,也能很快的得到回复响应。
此时你似乎感觉这样的响应速度已经不错了,同时也保证了生命指示灯(主程序)的持续运行。但这样是不能长久地,我可以给你看这样的一个结果:
注意右下角,我们以1000Hz的频率发送消息,你会注意到串口并不会每一条消息都回应,而且到后面你会看到如下情况
你会发现,我们不断地发送NO,但串口已经完全不再相应我们的消息,这就是非常非常常见的串口堵塞的问题,我自己也被坑过很多次。事实上,这也是完全能理解的情况,毕竟我们之前就定义10ms的串口接收时间:
HAL_UART_Receive(&huart1, receive_Buff1, sizeof(receive_Buff1), 10);
小于10ms时难免会出问题。当发生串口堵塞时,只有重新复位单片机才能解决问题。这就是没有用串口中断所导致的问题。
不仅如此,尤其是后面遇到要处理电机,调节PID的情况。PID参数几乎是针对某一个电机的专门参数,同时对相应频率的要求及其严格。一但你的主程序频率改变,PID参数可能就不再适用。更何况,主程序之后还要处理状态机这样的大型程序。因此,在主程序中接受并处理串口的消息是不太明智的做法。接下来,我将介绍串口中断的方式接受并处理串口消息。
5.串口空闲中断(DMA)接收消息
这一部分的代码修改就有点复杂了,中途可能会有疏忽的部分,如果这里大家照着我的改发现运行不对,可以参考一下我链接里的示例代码
串口中断是STM32当中最常用的中断情况,他可以保证当单片机的串口收到消息时,单片机可以放下手中的活(主程序),转头去处理串口收到的消息。同时,因为是在串口中断接受消息,所以我们需要先改变一下单片机的初始化配置。
勾选全局中断
添加DMA接受空闲中断(因为主要是接受的频率很高,发送一般不需要),这里添加空闲中断的主要目的还是为了避免单片机被频繁地触发串口中断,否则依旧会造成串口堵塞。选好后选择生成代码。
新建两个空白文档
添加文件路径
选择路径
右键左边部分,选择添加已有文件
此时我们就成功添加了外部的文件,接下来的代码顺序可以按照你自己的想法来,过程可以很灵活,我就不一一赘述了,全部的代码总结如下:
首先是串口中断回调函数以及串口DMA中断初始化函数
uart.h
#ifndef _UART_H_
#define _UART_H_
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
#include "mymain.h"
#include "string.h"
#include "stm32f1xx_hal_dma.h"
#define BUFF_NUM 2
extern uint8_t receive_Buff1[BUFF_NUM]; //接收数据直接区
extern uint8_t handle_Buff1[BUFF_NUM]; //接收数据缓冲区
extern void uart_init(void); //串口初始化函数
extern void USAR_UART_IDLECallback(void); //串口中断回调函数
#endif
uart.c
#include "uart.h"
// 接收数据直接区
uint8_t receive_Buff1[BUFF_NUM];
// 接收数据缓冲区
uint8_t handle_Buff1[BUFF_NUM];
// 用于计算字符长度
extern DMA_HandleTypeDef hdma_usart1_rx;
// 串口初始化
void uart_init(void)
{
// 开启串口1的串口中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// 串口1接受消息初始化,此时收到的结果会存储到receive_Buff1中
HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_Buff1, BUFF_NUM);
}
/*
* 中断处理回调函数
*/
void USAR_UART_IDLECallback(void)
{
// 判断是否是空闲中断
if (RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
{
// 清除空闲中断标志(否则会一直不断进入中断)
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 暂时关闭DMA
HAL_UART_DMAStop(&huart1);
// 计算接收到的数据长度
uint8_t receive_len1 = BUFF_NUM - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 把接收缓冲中的数据复制到处理缓冲中
memcpy(handle_Buff1, receive_Buff1, BUFF_NUM);
// printf("%s\r\n", handle_Buff1); // 测试函数:将接收到的数据打印出去
Data_Handle1();
// 清空接收缓冲区
memset(receive_Buff1, 0, receive_len1);
// 重新开启DMA
HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_Buff1, BUFF_NUM);
}
}
接下来需要把串口中断回调函数放到 stm32f1xx_it.c 文件中,这里存放了所有的中断源。放在这里,就能保证发生串口中断时会运行串口中断回调函数。找到下图的位置,并添加串口中断回调函数
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
USAR_UART_IDLECallback();
/* USER CODE END USART1_IRQn 1 */
}
接下来还要修改头文件引用,不然 stm32f1xx_it.c 找不到串口中断回调函数在哪里。现在我们来告诉他,找到 stm32f1xx_it.c 的如下位置并修改。
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f1xx_it.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "uart.h"
/* USER CODE END Includes */
接下来,我们要把串口初始化函数放在main.c文件中,保证主程序一开始就能初始化串口。找到如下位置并修改。
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
uart_init();
/* USER CODE END 2 */
同理,我们还需要告诉他初始化函数在哪里,修改头文件引用。找到 main.c 的如下位置并修改
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "uart.h"
/* USER CODE END Includes */
接下来,我们的串口就已经能够顺利接到消息了,接下来我们来编写串口消息处理函数。对于这一类比较重要且会经常改动的函数,我喜欢新建一个文件 mymain.c 和 mymain.h 来集中存放。当然你如果觉得麻烦,也可以存放在 main.c 或者别的文件里面
mymain.h
#ifndef _MAINMY_H_
#define _MAINMY_H_
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
#include "uart.h"
#include "string.h"
extern void Data_Handle1(void);
#endif
mymain.c
#include "mymain.h"
void Data_Handle1(void)
{
if ((handle_Buff1[0] == 'O') && (handle_Buff1[1] == 'K'))
{
printf("RIGHT\r\n");
}
if ((handle_Buff1[0] == 'N') && (handle_Buff1[1] == 'O'))
{
printf("ERROR\r\n");
}
}
同理,我们本来还应该告诉 uart.c 串口消息处理函数在这里,不过我已经在 uart.h 文件里告诉他了,就不用再处理了。接下来,我们再修改一下主程序。这里也是因为没有在主程序里接受串口消息了,因此主程序频率变了,滴答时钟也会变。这也体现主程序的周期的不稳定性。
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
uart_init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
int tick_time = 0;
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
tick_time++;
// chelk the time
// printf("tick_time: %d\r\n", tick_time);
if (tick_time % 7200000 == 0)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
}
if (tick_time % 7200000 == 7200000/2)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
}
// open the uart1 reciver
}
/* USER CODE END 3 */
到此,理论上我们的串口DMA中断就已经配置完毕了。可能里面还有些变量的调用没有声明,如果不对读者就再看看我的示例代码吧
首先,单片机复位后会用函数 uart_init(); 自动初始化串口,接着运行主程序。当发生串口中断时,会调用 stm32f1xx_it.c 文件里的 USAR_UART_IDLECallback(); 串口中断回调函数。接着串口中断回调函数会在处理好接受的消息后,调用串口消息处理函数 Data_Handle1(); 来处理消息。整个过程结束
接下来我们就来尝试一下DMA串口中断
我们用与之前一样的速度发消息,发现串口还是会丢一些,同时相应会有些延迟。这里大家要注意,相应延迟和丢消息是两个概念,丢数据意味着stm32完全忽略掉了这条消息,但相应延迟只是串口消息处理函数没有及时的把消息打印出来,但这条消息是执行了的。除此之外,我们也发现,串口是完全不会堵塞的。
接下来,我们进一步释放DMA的潜力,修改通信波特率为921600。再尝试一下,这里我们换一个串口助手会方便观察
这里注意修改波特率
这里看到接受和发送的比例约为3.5,即7:2。这也和我们的预期是一样的,说明在使用DMA后一个数据都没丢。OK,串口通信大功告成。如果你想开启更多的串口,方法同理。
如果有什么不足或者遗漏的地方,还望大家多多指正。如果对大家有所帮助的话,还望点个赞吧,谢谢。