一、环境
硬件:STM32F407ZGT6开发板
软件:STM32CubeMx、Keil5 MDK、串口调试助手
PS:前面实验部分的代码都是可以正常运行的,但是在学习过程中我也踩了很多坑(包括一些未弄明白的),我会记录在问题总结部分。
二、使用STM32CubeMx初始化配置
1.选择芯片型号
首先选择自己使用的板子对应的芯片型号,我这里使用的是STM32F407ZGT6的芯片。
2.配置SYS
点击System Core下拉栏的SYS,选择Debug调试,选择serial wire。
3.配置RCC
配置RCC时钟,由于这里使用的是外部高速晶振,点击HSE,选择Crystal/Ceramic Resonator。
4.配置时钟树
这里有几个需要特别关注的点:
第一,HSE频率要对应开发板的晶振频率,我使用的板子晶振频率为8MHz,如果这设置的频率不匹配,那么串口输出时可能会出现乱码;
第二,选择HSE时钟;
第三,选择锁相环;
第四,HCLK频率设置为最高的168MHz。
5.配置USART
点击Connectivity,选择USART1,模式选择为异步通信即Asynchronous,并且使能串口中断。
点击Configuration选项卡,可以看到波特率被自动配置为115200。
6.生成工程代码
三、串口通信实验
1.阻塞式发送
利用HAL_UART_Transmit()函数发送。
首先在main函数中定义一个需要发送的字符串:
uint8_t Transmit_data[]={"Hello CHINA!\r\n"}; //定义一个需要发送的字符串
然后在while语句中写两行代码:
HAL_UART_Transmit(&huart1,Transmit_data,sizeof(Transmit_data),10); //发送字符串
HAL_Delay(500); //延时500ms
代码编译成功之后,点击下载到单片机,打开串口调试助手,连接串口,可以看到串口在不停地打印字符串。
注意这里的波特率要和初始化配置的一致,否则会出现乱码。
2.重定向printf打印
通过对printf重定向,使用printf 可以用来串口打印数据。
首先在main.c文件中添加头文件"stdio.h"
然后在main.c文件中重定义fputc函数
//重定义fputc函数
int fputc(int ch,FILE *f)
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,100);
return ch;
}
接着在while语句中调用printf函数输出字符串。
printf("Hello everyone!\r\n");
为了避免使用半主机模式,可以先点击魔术棒,再点击Target选项,勾选Use Micro LIB。
如果这时出现编译报错,那么打开启动文件,注释掉第60行和第407行,编译一次后再取消注释,然后重新编译后就可以了。(不同的芯片可能这两行代码不一定是在第60行和第407行,注意内容正确。)
具体为什么这样操作一下就可以了,我也没太搞懂,如果有清楚的大佬,请在评论区留下您的宝贵回答。
然后将代码下载到开发板上,就可以输出字符串了。
3.阻塞式接收
利用HAL_UART_Receive()函数接收。
首先定义一个数组buffer,用来作字符接收缓冲区。
然后在while语句中调用HAL_UART_Receive()函数,接收串口发来的数据。
再然后在后面调用HAL_UART_Transmit()函数,将接收的数据发送到串口调试助手里面。
HAL_UART_Receive(&huart1,buffer,6,0xFFFF); //接收字符串
HAL_UART_Transmit(&huart1,buffer,6,0xFFFF); //发送字符串
这里特别注意两个点:
第一,超时时间设置的是最大值0xFFFF。因为这样可以阻塞住程序的执行,直到整个字符串传输完成。
第二,传输字符串长度这里我是相较于定义buffer字符串长度长1个单位的。
因为在接收数据时,每次接收完成后,接收数据寄存器(RDR)还保留着上一次循环发送的最后一个字符。
此处的字长+1后,每次循环结束留在接收数据寄存器(RDR)中的最后一个字符就是空,就不会出现接收的最后一个字符移动到第一个字符位置这种现象。
不加1发送第一次是正确的,后面就会丢失数据;加1发送接收正确,但是第二行往往是空行。
因为在C语言中,数组默认是以'\0'结尾的,只有这样才能输出正确且自动换行。(此解释存疑)
将代码编译后下载到开发板上,在串口助手的发送区输入字符,点击发送,可以看到接收区会显示Hello,证明接收成功,多发送几个字符,查看接收数据有无丢失。
这里仍然存在一点问题:第二行是空行。(未解决)
4.串口发送指令控制LED灯
首先定义一个长度为2个字节的数组用来发送指令。
然后在while语句中编写代码。若输入'R1',则红色LED灯亮起;若输入'G1',则绿色LED灯亮起。
printf("Input your order:\r\n");
HAL_UART_Receive(&huart1,receive_data,3,0xFFFF); //接收字符串
printf("Your order is:\r\n");
HAL_UART_Transmit(&huart1,receive_data,3,100); //发送字符串
GPIO_PinState state = GPIO_PIN_SET;
if(receive_data[1]=='1')
{
state = GPIO_PIN_RESET;
}
if(receive_data[0]=='R')
{
HAL_GPIO_WritePin(GPIOF, LED_RED_Pin, state);
}
else if(receive_data[0]=='G')
{
HAL_GPIO_WritePin(GPIOF, LED_GREEN_Pin, state);
}
HAL_Delay(1000); //延时1000ms
代码编译下载后运行,可以实现预期效果。
存在的问题:
9.6由于输出的字符串,首次是正确的,可以正确开启LED灯,但是后面就会乱套(未解决)
9.7发送接收命令过了一夜没问题了,但是要过5分钟左右才能执行下一次命令。
分析:可能是阻塞式发送的原因,
循环未执行完成(通过在循环末尾添加关闭LED的方式排除该可能)
或者数据发送流程未结束
5.中断式发送和接收
阻塞式发送和接收非常浪费CPU资源,必须阻塞住程序的执行,直到完成发送或接收、或者等待超时。另外在接收时,只能接收固定长度的数据。中断式发送和接收可以解决CPU资源浪费的问题。
首先中断式发送和接收使用的函数分别是HAL_UART_Transmit_IT()函数和HAL_UART_Receive_IT()函数。
相比于阻塞式发送和接收的HAL_UART_Transmit()函数和HAL_UART_Receive()函数,这两个函数的参数都少了一个超时时间。
HAL_UART_Transmit_IT(&huart1,receive_data,2); //中断发送字符串
HAL_UART_Receive_IT(&huart1,receive_data,2); //中断接收字符串
由于中断接收数据不会堵塞程序的运行,那么还没有等到接收到数据,程序就会向下继续执行,这样就会导致,当程序执行到下次循环时,可能上次的数据还没有接收完成,便又开启串口中断接收。为了避免冲突,将中断接收函数放置在while循环外只执行一次。
中断模式的HAL_UART_Receive_IT()函数不会堵塞,在开启中断接收后,程序就继续向下执行了,这时不能直接对数据进行分析,因为数据很可能还没有接收完成。
使用中断处理函数可以在数据接收完成后再对数据进行分析处理。
在stm32f4xx.it.c文件中找到串口的中断处理函数。
然后在HAL_UART_IRQHandler()函数中找到接收完成中断回调函数HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)。
由于在这里是弱定义的,所以在main.c文件中重新定义该函数,并将主要的执行语句放入该函数体内。特别注意,由于中断接收函数 HAL_UART_Receive_IT()只执行了一次,为了能够继续输入命令控制LED,所以在函数体末尾再执行一次中断接收函数 HAL_UART_Receive_IT()开启接收中断。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_IT(&huart1,receive_data,2); //中断发送字符串
GPIO_PinState state = GPIO_PIN_SET;
if(receive_data[1]=='1')
{
state = GPIO_PIN_RESET;
}
if(receive_data[0]=='R')
{
HAL_GPIO_WritePin(GPIOF, LED_RED_Pin, state);
}else if(receive_data[0]=='G')
{
HAL_GPIO_WritePin(GPIOF, LED_GREEN_Pin, state);
}
HAL_UART_Receive_IT(&huart1,receive_data,2); //中断接收字符串
}
然后将receive_data定义为全局变量。
代码编译下载后运行,可以实现用串口控制LED灯的预期效果,效果不错。
提示:这里的字长都是和定义的数组长度是一致的,输出也不会出错,跟之前的解释有些出入,暂时还没搞懂原因。
9.7如果收发2个字节的字符串,不会出错。3个字节及以上的就会出错。
3个字符的字符串是先输出第1个字符,再换行输出前2个字符,接着输出所有的字符。
4个字节的字符串是先输出前2个字符,又输出所有的字符,又换行输出2个字符。
6.串口DMA模式
DMA(Direct Memory Access),直接内存访问。DMA用于在外设与存储器之间以及存储器与存储器之间提供高速数据传输。可以在无需任何 CPU 操作的情况下通过 DMA 快速移动数据。这样能节省CPU资源。
首先在STM32CubeMX中选择Connectivity-USART1-DMA Settings中添加2个DMA通道。
使用HAL_UART_Transmit_DMA()和HAL_UART_Receive_DMA()函数来收发数据。
将原本的HAL_UART_Transmit_IT()和HAL_UART_Receive_IT()函数替换成HAL_UART_Transmit_DMA()和HAL_UART_Receive_DMA()函数。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_DMA(&huart1,receive_data,2); //中断发送字符串
GPIO_PinState state = GPIO_PIN_SET;
if(receive_data[1]=='1')
{
state = GPIO_PIN_RESET;
}
if(receive_data[0]=='R')
{
HAL_GPIO_WritePin(GPIOF, LED_RED_Pin, state);
}else if(receive_data[0]=='G')
{
HAL_GPIO_WritePin(GPIOF, LED_GREEN_Pin, state);
}
HAL_UART_Receive_DMA(&huart1,receive_data,2); //中断接收字符串
}
代码编译下载后运行,可以实现用串口控制LED灯。
老问题:2个字节的字符串输出没问题,3个字节及以上的字符串输出会出错,原因不明。
7.收发不定长数据
利用串口空闲中断可以实现收发不定长数据。
使用HAL库的一个扩展函数HAL_UARTEx_ReceiveToIdle_DMA()来接收数据。
首先给receive_data数组一个较大的长度。
然后将HAL_UART_Receive_DMA()函数替换为HAL_UARTEx_ReceiveToIdle_DMA()函数。
这个函数对应的中断回调函数是HAL_UARTEx_RxEventCallback(),这个回调函数同样是弱定义的,在main.c文件中进行定义。
在函数体内部首先判断中断触发源,再使用DMA发送数据,然后使用上述扩展函数进行发送。
特别提示:阻塞模式和中断模式的ReceiveToIdle函数没什么特别注意的点,但是对于DMA模式的ReceiveToIdle函数来说,除了串口空闲中断以外,DMA的“传输过半中断”也会触发RxEventCallback回调函数,若接收的数据长度达到设置的receive_data数组的一般时,也会触发一次RxEventCallback中断回调。
在本实验中,此功能无甚用处,所以将DMA的“传输过半中断”进行关闭。
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart == &huart1) //判断中断触发源
{
HAL_UART_Transmit_DMA(&huart1,receive_data,Size); //发送字符串
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receive_data,sizeof(receive_data));//接收字符串
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT); //关闭传输过半中断
}
}
在主函数中只运行一次的接收字符串函数下也得关闭传输过半中断。
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receive_data,sizeof(receive_data)); //接收字符串
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT); //关闭传输过半中断
编译时,会发现hdma_usart1_rx未定义,这是因为这个变量定义在其他文件中,所以需要在main.h文件中进行声明。
extern DMA_HandleTypeDef hdma_usart1_rx;
编译下载后运行,就可以发送和接收不定长的数据了。
四、遇到的问题总结
1.串口乱码问题(已解决)
第一,上位机和板子端的波特率不一致。这里的上位机指的是串口调试助手。
第二,注意CubeMx中HSE的值务必要和自己板子上实际晶振大小一致。由于CubeMx默认F407使用的晶振频率是25MHz,所以我在一开始的时候也是输出乱码,后面改为8MHz,成功解决乱码问题。
2.字符串接收问题(未解决)
这是原本的阻塞式接收的代码,这里存在两个问题,下面会详细说明。
第一,超时时间设置太短。
超时时间设置为100ms,这样只能传输第一个字符,而后续的字符会丢失,并且这个字符串首字符会持续不断地传输,因为接收完第一个字符已经达到超时时间,这时就会将字符发送到上位机(这里指的是PC端的串口调试助手),但是由于字符串还没有传输完成,所以又会进行下一次循环,一直持续下去。
解决办法:将超时时间更改为0xFFFF,但仍然存在问题。
第二,传输数据的寄存器会保留上一次发送的最后一个字符。
这里上一次发送的最后一个字符是空格,就会如下图发送一次,接收到上一次的最后一位加这一次的前4位。
解决办法:将传输字符串长度相较于定义长度加1。
最后就可以正常输出,结果如实验部分--阻塞式接收的结果。
3.串口2和3用不了(未解决)
9.6又试了一次,同样的配置,串口3好像就是用不了。
怀疑是串口被烧了,后面找个万用表测一下。
目前存在的问题总结:
1.字符串长度要+1,不然输出混乱,但是+1后,串口助手接收到的数据第二行是空行(阻塞式收发数据)
2.LED灯控制,响应第一个命令后,5分钟左右才能响应第二个命令。(阻塞式收发数据控制LED灯)
3.两个字节数据收发正常,三个字节及以上的字符串输出就会出错(普通中断模式和DMA中断模式)
4.串口2、3无法使用。