====>>> 文章汇总(有代码汇总) <<<====
正点原子Mini板,主控 STM32F103RCT6.
配置debug模式(如果需要ST-Link下载及调试可以勾选)
配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)
编写main.c
代码
int main(void)
{
/* USER CODE BEGIN 1 */
char light_on[11] = "Light on \r\n";
char light_off[12] = "Light off \r\n";
char rec_buf[1] = {0};
/* USER CODE END 1 */
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN WHILE */
while (1)
{
// 串口1、接收数据的数组、接收的长度、等待时间
if(HAL_OK == HAL_UART_Receive(&huart1, (uint8_t *)rec_buf, 1, 0xFFFF))
{
if(rec_buf[0] == 'A')
{
// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
HAL_UART_Transmit(&huart1, (uint8_t *)light_on, 11, 0xFFFF);
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
}
else
{
// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
HAL_UART_Transmit(&huart1, (uint8_t *)light_off, 12, 0xFFFF);
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
编译、烧录、查看结果。
效果:使用串口调试助手
电脑发送一个字符A,单片机收到后 发送 Light on 给电脑,同时LED亮。
电脑发送一个字符B(其他字符),单片机收到后 发送 Light off 给电脑,同时LED灭。
和分析中断的过程一样。打开工程,在stm32f1xx_it.h
中可以看到函数USART1_IRQHandler
,显然,这是串口中断处理函数,在中断处理函数中又调用了函数HAL_UART_IRQHandler(&huart1);
然后去看看函数HAL_UART_IRQHandler(&huart1);
都干了什么事。在HAL_UART_IRQHandler(&huart1);
函数中考虑了很多情况,从注释可以看出,不出错误的话,会调用函数UART_Receive_IT(huart);
然后就返回了。
因此,我们看看函数UART_Receive_IT(huart);
,在这个函数中也分了很多很多情况,我们直接看最后,最后有个函数HAL_UART_RxCpltCallback(huart);
,从注释可以看出,这是个接收完成的回调函数。(学完配置以后,一定要看看7.1小节,关于这个函数的触发条件说明)
可以看到这个函数是个弱函数,用户可以再次定义该函数。也就是说,我们可以重新定义这个函数,并在函数中编写我们处理中断的逻辑。
注释写的也很清楚:
当需要回调时,不应修改此函数,HAL_UART_RxCpltCallback可以在用户文件中实现
这段放在哪都行。大的工程可以创建一个文件放进去;这里直接放在main.c中了。
/* USER CODE BEGIN PV */
char light_on[11] = "Light on \r\n";
char light_off[12] = "Light off \r\n";
char rec_buf[1] = {0};
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断是不是串口1 中断
if(huart->Instance == USART1)
{
if(rec_buf[0] == 'A')
{
// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
HAL_UART_Transmit(&huart1, (uint8_t *)light_on, 11, 0xFFFF);
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
}
else
{
// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
HAL_UART_Transmit(&huart1, (uint8_t *)light_off, 12, 0xFFFF);
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
}
// 重新使能接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 1);
}
}
/* USER CODE END PFP */
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* Infinite loop */
/* USER CODE BEGIN WHILE */
// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 1);
// 发送开始提示信息
HAL_UART_Transmit(&huart1, (uint8_t *)"start...\r\n", 10, 0xFFFF);
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
编译、烧录、查看结果。
配置DMA
DMA (
Direct Memory Access
)直接存储器访问,可以不使用CPU,将数据在存储器与外设之间传输(也可以从存储器 到 存储器)。
STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中),STM32F103RCT6 有两个 DMA 控制器, DMA1 和 DMA2,DMA1 有 7 个通道。DMA2 有 5 个通道。
DMA Request:
- USART1_TX:表示在发送数据的时候使用 DMA通道。
- USART1_RX:表示在接收数据的时候使用 DMA通道。
Mode:
- Normal 模式:表示 CPU 发起 DMA 传输请求,就把数据通过DMA通道传输出去,传输完成之后会触发中断;
- Circular模式,表示会循环把数据传输出去,传输完成之后从头再来。
Increment Address:这里在内存上勾选,没勾选外设,因为外设(这里是串口)地址是固定的,只需要把数据的 起始地址传输过去,地址不断累加就可以了。
勾选中断(DMA中断优先级设置的比串口中断要低,不过其实不低也能运行)。
/* USER CODE BEGIN PV */
char buf[13] = "running... \r\n";
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN WHILE */
// 开启DMA接收数据通道,将串口数据存到buf数组。
// 这里设置的循环模式,会不断接收串口数据到内存。
HAL_UART_Receive_DMA(&huart1, (uint8_t *)buf, 13);
// 串口其他 DMA 相关函数
// HAL_UART_DMAResume(&huart1); 恢复串口DMA
// HAL_UART_DMAPause(&huart1) 暂停串口DMA
// HAL_UART_DMAStop(&huart1); 结束串口DMA
while (1)
{
// 开启DMA发送传输通道
// 这里设置的普通模式,发送一次就关闭了。延迟一段时间后,重新打开发送。
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)buf, 13);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
这里有个bug。
- MX_DMA_Init();
- MX_USART1_UART_Init();
这两个初始化,必须DMA在前,USART在后,否则编译通过,但是运行会卡死。
如果一开始就设置了DMA和串口,一般没啥问题。但是如果一开始只设置了串口,生成代码后,又加了DMA,就容易出现 DMA在后面的情况。
另外:可以通过设置,来设置先后顺序。
编译、烧录、查看结果。
为了方便发送数据,我们使用printf
函数发送数据,但是使用此函数需要先进行配置。
printf
函数定义在
头文件中,printf 函数根据 format 字符串给出的格式打印输出到 std::out(标准输出)中。
printf 函数会调用更底层的 I/O 函数:fputc
去逐个字符打印。fputc 也定义于头文件
因此,我们如果想用 printf 打印数据到串口,只需要重新定义fputc函数,在fputc的函数中将数据通过串口发送,称之为:fputc重定向或者printf重定向。
配置还是上面三种的配置,随便啦。
MicroLib是对标准C库进行了高度优化之后的库,供MDK默认使用,相比之下,MicroLIB的代码更少,资源占用更少。
步骤一:在MDK中使用MicroLib重定向printf
步骤二:在生成的usart.c中添加重定向代码。这里放到最后
/* USER CODE BEGIN 1 */
/**********************printf重定向****************************/
// 需要调用stdio.h文件
#include
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/* USER CODE END 1 */
main.c
while (1)
{
printf("Hello, %s \r\n", "world"); // 字符串
printf("Test int: i = %d \r\n", 100); // 整数
printf("Test float: i = %f \r\n", 1.234); // 浮点数
printf("Test hex: i = 0x%2x \r\n",100); // 16进制
printf("中文测试 \r\n"); // 中文
HAL_Delay(2000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
效果验证
编译、烧录、查看结果。
不用勾选 Use MicroLIB,直接在 usart.c 文件中添加以下代码。
// 需要调用stdio.h文件
#include
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
x = x;
}
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
main.c
while (1)
{
printf("Hello, %s \r\n", "world"); // 字符串
printf("Test int: i = %d \r\n", 100); // 整数
printf("Test float: i = %f \r\n", 1.234); // 浮点数
printf("Test hex: i = 0x%2x \r\n",100); // 16进制
printf("中文测试 \r\n"); // 中文
HAL_Delay(2000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
效果验证
每隔两秒,发送数据。
PS:在其他文件中使用printf会有警告 unction “printf” declared implicitly 出现,不影响使用。如果不想看到警告,可以在调用的文件中添加#include
串口中断函数
每接收到一个字节,就会进入一次串口中断函数。如果一个数据帧发送了多个字节,就会连续进入多次中断处理函数。
比如:通过串口助手给单片机发送"0123456789"(不要勾选发送新行),则单片机会连续进入10次void USART1_IRQHandler(void)
中断处理函数。
接收完成回调函数
每接收完成一次,会执行一次接收完成回调函数。
前面中断章节中,说了接收完毕后会执行接收完成回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
,但是没有具体说明,什么时候才算接收完成。
可以看下面这行代码,显然,这里是想接收10个字节,并把它放到rec_buf数组中。那么,接收够10个字节,就表示接收完成了。
// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
那如果,我只发送5个字节呢?那就只会进入5次中断,不会执行接收完成回调函数,当再次发送5个字节的时候,就会在执行第5次中断的时候,执行接收完成回调函数。
再如果,发送8个字符呢?那就只会进入8次中断,不会执行接收完成回调函数,当再次发送8个字节的时候,会在执行2次中断并执行回调函数,然后再次执行一次中断函数就卡死了…
示例
串口配置和中断章节的配置一样:打开串口,勾选中断即可。
main.c
char rec_buf[10] = {0};
volatile uint8_t num = 0; // 记录进入中断的次数
volatile uint8_t numback = 0; // 记录进入回调的次数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断是不是串口1 中断
if(huart->Instance == USART1)
{
printf("已经进入了 %d 次中断函数了 \r\n", num);
printf("第 %d 次进入回调 \r\n", ++numback);
// 重新开启接收
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
while (1)
{
printf("已经进入了 %d 次中断函数了 \r\n", num);
HAL_Delay(1000);
}
}
stm32f1xx_it.c
extern volatile uint8_t num;
void USART1_IRQHandler(void)
{
num++; // 记录进入中断的次数
HAL_UART_IRQHandler(&huart1);
}
发送“1234567890”10个字符(10个字节),可以看到,会进入10次中断,在最后一次中断时,刚好接收够10个字节,也就执行了回调函数。
而当发送“12345”5个字符(5个字节)时,进入了5次中断,但是并没有接收完成,如果再次发送5个字符,则再进入5次中断,再第5次中断时接收完成,执行了回调函数。
而如果发送“12345678”8个字符(8个字节)时,进入了8次中断,但是并没有接收完成,如果再次发送8个字符,第二次中断会执行回调函数,并再次进入中断,之后再点发送就没用了。。。
总之,前面的三种方案,只能处理数据长度比较固定的数据。
先说要一下串口的中断。主要说两个串口接收中断、串口空闲中断。
总结:
接收中断 | 空闲中断 | |
---|---|---|
处理函数 | USARTx_IRQHandler() | USARTx_IRQHandler() |
回调函数 | HAL_UART_RxCpltCallback() | HAL库没有提供 |
USART状态寄存器中的位 | UART_IT_RXNE | UART_IT_IDLE |
触发条件 | 接受到一个字节数据触发一次 | 接受完一帧数据又过了一个字节时间没有接收到数据 |
在上面的程序中,当DMA串口接收开始后,DMA通道会不断的将发送来的数据转移到内存,也就是说接收程序一直在运行,那该如何判断串口接收是否完成从而及时关闭DMA通道?如何知道接收到数据的长度,以便于接收不定长的呢?答案便是使用串口空闲中断。
解决办法
代码(配置就是DMA模式的配置)
main.c
/* USER CODE BEGIN PV */
// 自己定义两个变量
uint8_t rx_buffer[100]; // 接收数据的数组
volatile uint8_t rx_len = 0; // 接收数据的长度
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN WHILE */
//开启空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// 开启DMA接收数据通道,将串口数据存到buf数组。
HAL_UART_Receive_DMA(&huart1, (uint8_t *)rx_buffer, 100);
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
stm32f1xx_it.c
/* USER CODE BEGIN PV */
// 定义在main.c中,在此处声明
extern uint8_t rx_buffer[100]; // 接收数据的数组
extern volatile uint8_t rx_len; // 接收数据的长度
/* USER CODE END PV */
/**
* @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 */
// 获取IDLE状态
uint8_t tmp_flag = __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE);
if((tmp_flag != RESET)) // 判断接收是否结束
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲中断标志
HAL_UART_DMAStop(&huart1); // 停止DMA通道
// 查询DMA剩余传输数据个数
uint8_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 最大接收长度 - 剩余长度 = 已发送长度
rx_len = 100 - temp;
/* === 在下面写自己的处理逻辑(注意这里是中断,尽量简短) === */
// 发送接收到的数据
HAL_UART_Transmit_DMA(&huart1, rx_buffer, rx_len);
/* === 在上面写自己的处理逻辑(注意这里是中断,尽量简短) === */
// 重新开启DMA
HAL_UART_Receive_DMA(&huart1,rx_buffer,100);
}
/* USER CODE END USART1_IRQn 1 */
}
代码有点长,代码链接在主页,代码有详细注释和使用说明。