前言
在微控制器的开发中,串口打印文本的功能非常重要!在每一款微控制器都会有这样的功能,在我们的开发中,串口具有举足轻重的地位。另外STM32F4里面拥有“USART模块”,即异步同步收发器,除了可以串口输出/接受数据,还可以使用在RS232、RS485、智能卡、LIN协议、调制解调等功能。本文硬件基于正点原子STM32F4探索者开发板,采用STM32F407ZGT6主控芯片,本文的代码运行环境为STM32CubeIDE,实现了串口打印和接收数据,并实现了printf和scanf的函数功能,可以酌情移植到其他硬件电路中。
串口配置
串口配置分以下步骤完成。
串口通信配置
这里采用异步通信模式,没有使用硬件流控制,波特率为115200,8位数据位,没有奇偶校验,1位停止位,接收和发送都打开,采用16位的过采样模式。
关于过采样,实际操作的寄存器是USART_CR1的Bit15,实际原理可以看一下参考手册,这里不再赘述。
这里根据原理图的连接状态,选择引脚重映射。目前没有使用DMA,但开启了中断。
重映射引脚配置
中断配置
如果需要使用中断接受或发送,可以使能中断,并配置优先级。
将中断打开,优先级设置的比滴答定时器低一些。(数字越小,优先级越高。能不能打断别的中断,看的是抢占优先级,数字越小越优先执行)
至此,串口的配置结束。
测试代码
先测试一下简单的发送数据
在main.c里面自动生成了串口和GPIO的初始化操作,之后我们会分析串口初始化里面的代码,GPIO的初始化简单,就不在这里赘述了。
先说明一下main.h里面定义的宏
#define STANDBY_MODE_TEST 0/*Notice: If you use PA0 as a KEY_UP button, This value mast be 0*/
#define UART_RXBUFFER_SIZE (uint16_t)1 //When usart receive ONE Byte, save data to a variable
#define USART_RX_TOTALLY_BUFFER 100
main函数执行之前的全局变量定义
在main函数之前,定义全局变量:
uint8_t USART_RX_BUFFER[USART_RX_TOTALLY_BUFFER];//Totally Save RX datas
uint8_t aRxBuffer[UART_RXBUFFER_SIZE];//When usart receive ONE Byte, save data here! Notice: this array only has ONE element.
extern uint16_t USART_RX_STA;/*Use this variable as a register; Bit15--have received complete or NOT; Bit14--receive 0x0d or NOT; Bit13-Bit0--the subscript of receiving array*/
在usart.c里面定义的变量
extern uint8_t aRxBuffer[UART_RXBUFFER_SIZE];//When usart receive ONE Byte, save data here! Notice: this array only has ONE element.
extern uint8_t USART_RX_BUFFER[USART_RX_TOTALLY_BUFFER];//Totally Save RX datas
uint16_t USART_RX_STA = 0;/*Use this variable as a register; Bit15--have received completely or NOT; Bit14--receive 0x0d or NOT; Bit13-Bit0--the subscript of receiving array*/
初始化串口和GPIO之后打开中断接收使能
在main函数中,进入while(1)死循环之前,打开串口的中断接收。
HAL_UART_Receive_IT(&huart1, (uint8_t *)aRxBuffer, UART_RXBUFFER_SIZE);
下面讲一下HAL_UART_Receive_IT函数的参数:&huart1就是USART1的句柄,aRxBuffer指针就是存放接收收到数据的地址,最需要前面注意的参数是:UART_RXBUFFER_SIZE(前面定义此宏为1)。意味着,接收到1个字节的数据,就要执行一次接收完成回调函数HAL_UART_RxCpltCallback(huart)。
【注意:每接收到一个字节数据,就会触发中断,执行USART1_IRQHandler(void) 中断处理函数,但我们可以人为设置,当接收到多少字节数据的时候,微控制器STM32调用并执行到HAL_UART_RxCpltCallback(huart) 函数,这个函数是需要我们重写的,以此达到通知我们的目的。这个过程是HAL库的内部逻辑完成的。】
由上面这行代码,告诉HAL库,接收到1字节的数据就要调用HAL_UART_RxCpltCallback(huart) 函数来通知我们。而这个函数我们是可以重定义的,里面写我们自己的逻辑。
串口打印数据测试
写在while(1)死循环的代码:
char send[] = "Hello World\r\n";
HAL_UART_Transmit(&huart1, (uint8_t *)send, sizeof(send), 1000);
或者我们直接操作寄存器:
USART1->DR = 'A';
while(USART1->SR & 0x40 == 0)
{
/*wait*/
}
这里的0x40是在检测是否发完了。
这时候,使用TTL转USB模块,连接电脑,就可以看到串口调试助手上接收到数据了。
将接收到的数据发送回去
编写接收完成回调函数
在usart.c里添加代码:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if((USART_RX_STA & 0x8000) == 0)//have NOT received completely
{
if(USART_RX_STA & 0x4000)//接收到了0x0d,意味着下一个字符应该是0x0a。回车由这两个字符组成
{
if(aRxBuffer[0] != 0x0a)
{
USART_RX_STA = 0;//receive ERROR! receive again
}
else
{
USART_RX_STA |= 0x8000;//receive completely
}
}
else
{
if(aRxBuffer[0] == 0x0d)//received 0x0d
{
USART_RX_STA |= 0x4000;
}
else
{
/*Have NOT receive "ENTER" */
USART_RX_BUFFER[USART_RX_STA & 0x3FFF] = aRxBuffer[0];/*Store data to receiving array, the subscript is given by USART_RX_STA[13-0]*/
USART_RX_STA++;/*the subscript given by USART_RX_STA[13-0] plus ONE*/
/*At this time the Bit15 and Bit 14 are both 0*/
if(USART_RX_STA > USART_RX_TOTALLY_BUFFER - 1 )/*receive overflow*/
{
USART_RX_STA = 0;/*receive again*/
}
}
}
}
else
{
/* The handle code is in main function, when Receve Complete*/
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)aRxBuffer, UART_RXBUFFER_SIZE);
}
有没有留意到,这个函数就是前面提到的——接收完成回调函数?由于我们设置,接收到1字节数据就执行此函数一次,那么我们在这里写了逻辑,来完成我们的目标:
如果串口接收到很长的字符串,我们可以将其保存在一个字符数组中,以备下一步将其发送出去。
【这里参考了正点原子的代码】
详解代码逻辑
在讲解下面的内容之前,先分析一下上面在中断回调函数里的代码逻辑,方便大家理解和使用这里的代码。
首先,在开启接收中断的时候,大家应该还记得,我们希望STM32接收到1字节的数据就调用中断回调函数来通知我们。所以,我们就在这里处理这刚刚接收到的1字节数据,它存储在指针aRxBuffer所指向的1字节内存中。
USART_RX_STA 这个变量在这里当寄存器使用了,它的BIT15代表的意思是接收字符串已经完成;BIT14代表接收到“半个回车”,也就是接收到了0x0d。所谓“半个回车”,是说,回车符是由两个字符组成的。没错,你在串口调试助手上点击发送,并且已经勾选了“发送新行”,那就会自动在字符串后面发送两个字符:0x0d 0x0a
当然我们在串口调试助手上发送的字符串不只是一个回车。设想一个场景,我们在串口调试助手上发送了:ab。那么在我们的STM32的串口上,会受到ab+0x0d+0x0a,一共4个字符。收到a的时候,判断一下,之前没有收到“半个回车”,那么这个字符是普通字符,就存储在我们的字符数组USART_RX_BUFFER。
至于存储在这个数组的那个下标的位置呢?由USART_RX_STA 的BIT0~BIT13里面存储的数字决定。可想而知,这是2的14次方个下标的容量,已经完全满足当下标的条件了,是不是?简单计算一下,2^14 = 16384,我们最大可以使用到USART_RX_BUFFER[16383],但我们不会定义这么大的数组。
一旦放进字符数组USART_RX_BUFFER中,就把下标加1,也就是USART_RX_STA 加1。直到收到回车为止。当收到了回车,USART_RX_STA 的BIT15就是1,就不会再存储数据进去了。下面就是在主函数中检查这个BIT是不是1,如果是1,那么就可以从字符数组中取出来接收的字符串,然后进行处理了,你想发送回去也行,想进一步处理代表什么含义也可以,完全由你!
main.c函数里加入逻辑代码
在进入while死循环之前,定义times变量用于计数。
uint16_t times=0;
在while(1)死循环中,写如下代码:
if((USART_RX_STA&0x8000) != 0)
{
len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
uint8_t i = len;
while(i--)
{
char send0[] = "\r\nwhat you send is:";
HAL_UART_Transmit(&huart1,(uint8_t*)send0,sizeof(send0),1000); //发送接收到的数据
HAL_UART_Transmit(&huart1,(uint8_t*)USART_RX_BUFFER,len,1000); //发送接收到的数据
}
USART_RX_STA=0;
}
else
{
times++;
if(times%5000==0)
{
times = 0;
char sss[] = "\r\n学而时习之, 不亦说乎!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t *)sss, sizeof(sss),1000);
}
}
至此,已经将代码编写完毕!
实验现象
prinf和scanf函数的功能实现
下面增加printf和scanf函数的实现代码。在Keil中,通过写下面的代码,并且在Use MicoLib打勾来实现。但在STM32CubeIDE里不行。
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (uint8_t) ch;
return ch;
}
下面是方法:
新建两个文件,retarget.c和retarget.h。下面是代码:
retarget.c文件
// All credit to Carmine Noviello for this code
// https://github.com/cnoviello/mastering-stm32/blob/master/nucleo-f030R8/system/src/retarget/retarget.c
#include <_ansi.h>
#include <_syslist.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/times.h>
#include <limits.h>
#include <signal.h>
//#include <../Inc/retarget.h>
#include <retarget.h>
#include <stdint.h>
#include <stdio.h>
#if !defined(OS_USE_SEMIHOSTING)
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
UART_HandleTypeDef *gHuart;
void RetargetInit(UART_HandleTypeDef *huart) {
gHuart = huart;
/* Disable I/O buffering for STDOUT stream, so that
* chars are sent out as soon as they are printed. */
setvbuf(stdout, NULL, _IONBF, 0);
}
int _isatty(int fd) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 1;
errno = EBADF;
return 0;
}
int _write(int fd, char* ptr, int len) {
HAL_StatusTypeDef hstatus;
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return len;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _close(int fd) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 0;
errno = EBADF;
return -1;
}
int _lseek(int fd, int ptr, int dir) {
(void) fd;
(void) ptr;
(void) dir;
errno = EBADF;
return -1;
}
int _read(int fd, char* ptr, int len) {
HAL_StatusTypeDef hstatus;
if (fd == STDIN_FILENO) {
hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return 1;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _fstat(int fd, struct stat* st) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
st->st_mode = S_IFCHR;
return 0;
}
errno = EBADF;
return 0;
}
#endif //#if !defined(OS_USE_SEMIHOSTING)
retarget.h文件
// All credit to Carmine Noviello for this code
// https://github.com/cnoviello/mastering-stm32/blob/master/nucleo-f030R8/system/include/retarget/retarget.h
#ifndef _RETARGET_H__
#define _RETARGET_H__
#include "stm32f4xx_hal.h"
#include <sys/stat.h>
void RetargetInit(UART_HandleTypeDef *huart);
int _isatty(int fd);
int _write(int fd, char* ptr, int len);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _read(int fd, char* ptr, int len);
int _fstat(int fd, struct stat* st);
#endif //#ifndef _RETARGET_H__
main.c文件中增加的代码
#include "retarget.h"
完成初始化串口和GPIO之后,就可以调用如下函数:
RetargetInit(&huart1);
之后在while(1)里面写如下代码测试:
printf("\r\n输入三个数字,以空格间隔:\r\n");
scanf("%d %d %d", &a, &b, &c);
printf("\r\n输入的数字为:%d, %d, %d\r\n", a, b, c);
测试结果
参考文献
[1]关于printf和scanf函数的实现,参考文章https://blog.csdn.net/qq_42212961/article/details/105803129
[2]关于接收串口信息并将其发送回去的代码,参考正点原子的官方例程
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/smart_boy__/article/details/148319101
|
|