串口发送
下面这个是我们的USB转串口的模块,这里有个跳线帽,上节也说过,要插在VCC和3V3这两个脚上,选择通信的TTL电平为3.3V,然后通信引脚,TXD和RXD,要接在STM32的PA9和PA10口。为什么是这两个口呢,我们看一下引脚定义表就知道USART1的TX是PA9, RX是PA10,我们计划用USART1进行通信,所以就选这两个脚。TX和RX交叉连接,这边一定要注意,别接错了。然后,两个设备之间要把负极接在一起,进行共地,一般多个系统之间互连,都要进行共地。最后,这个串口模块和STLINK都要插在电脑上,这样,STM32和串口模块都有独立供电,所以这里通信的电源正极就不需要接了。
USART1的TX是PA9, RX是PA10
当然我们第一个代码,只有STM32发送的部分,所以,通信线只有这个发送的有用,另一根线,第一个代码没有用到,暂时可以不接,在我们下一个串口发送+接收的代码,两根通信线就都需要接了。所以我们把这两根通信线一起都接上吧,这样两个代码的接线图是一模一样的。
库函数
//这个两个用来配置同步时钟输出
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
//可以开启USART到DMA的触发通道
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);//发送数据,SendData就是写DR寄存器
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);//接收数据,ReceiveData就是读DR寄存器
第一步,开启时钟,把需要用的USART和GPIO的时钟打开
第二步,GPIO初始化,把TX配置成复用输出,RX配置成输入
第三步,配置USART,直接使用一个结构体,就可以把这里所有的参数都配置好了
第四步,如果你只需要发送的功能,就直接开启USART,初始化就结束了。如果你需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就行了。
那初始化完成之后,如果要发送数据,调用一个发送函数就行了;如果要接收数据,就调用接收的函数;如果要获取发送和接收的状态,就调用获取标志位的函数,这就是USART外设的使用思路。
串口助手
这里必须要插上串口,且串口是空闲的状态,才能扫描到对应的串口号。这里的参数需要和代码初始化参数一致。波特率:9600,数据位:8位,停止位,1位,校验位:无。然后打开串口。
这里是NEX模式,以原始数据的形式显示。
显示字符串,用文本模式
数据模式
ASCII字符集
做数据和字符串相互转换时,需要查一下这个表。
介绍printf函数移植方法
在使用printf之前,打开工程选项,在TArget选项中,把Use MicroLIB勾上
汉字乱码接解决方案
显示汉字的操作方法,printf打印会乱码,解决方案:这个的汉字编码格式,选择的是UTF8,最终发送到串口,汉字会以UTF8的方式编码,最终串口也得选择UTF8,才能解码正确。
UTF8不乱码的方案,比如这里写,“你好,世界”字符串,这样直接写汉字,编译器有时会报错,解决方法,打开工程选项,C/C++ 这里杂项控制栏。 写好了之后,在串口助手这,选择编码方式,是UTF8。这是UTF8解决方案
但是UTF8可能有些软件兼容性可能不好,所以第2种方式是,切换为GB2312编码。在这里选择配置。完成之后,汉字还是UTF8,需要把“你好世界“删掉,再把当前文件关掉,重新打开,等字体变为宋体了,编码格式才是改过来的,在重新输入汉字,这样编码才是GB2312。
打开串口助手,在串口助手选择GBK编码,一般window软件默认是GBK编码。
总结:要么keil和串口助手都选择UTF8,且keil加上 --no-multibyte-chars参数。要么都使用GB开头的中文编码格式,参数不用加。这就是汉字乱码接解决方案。
程序发送现象
mian.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Serial_Init(); //串口初始化
/*串口基本函数*/
Serial_SendByte(0x41); //串口发送一个字节数据0x41
uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45}; //定义数组
Serial_SendArray(MyArray, 4); //串口发送一个数组
Serial_SendString("\r\nNum1="); //串口发送字符串。转义字符\r\n是换行
Serial_SendNumber(111, 3); //串口发送数字
/*下述3种方法可实现printf的效果*/
/*方法1:直接重定向printf,但printf函数只有一个,此方法不能在多处使用*/ //你重定向到串口1了,到了串口2再用就没了。
printf("\r\nNum2=%d", 222); //串口发送printf打印的格式化字符串
//需要重定向fputc函数,并在工程选项里勾选Use MicroLIB
/*方法2:多个串口想用printf。sprintf可以把格式化字符输出到一个字符串里。 使用sprintf打印到字符串,再用串口发送字符串,此方法打印到字符串,之后想怎么处理都可以,可在多处使用*/
char String[100]; //定义字符串
sprintf(String, "\r\nNum3=%d", 333); //第一个参数是打印输出的位置,指定打印String 。 使用sprintf,把格式化字符串打印到字符数组
Serial_SendString(String); //把字符串String通过串口发送出去
/*方法3:将sprintf函数封装起来,实现专用的printf,此方法就是把方法2封装起来,更加简洁实用,可在多处使用*/
Serial_Printf("\r\nNum4=%d", 444); //串口打印字符串,使用自己封装的函数实现printf的效果
Serial_Printf("\r\n");
while (1)
{
}
}
Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h> //串口封装
void Serial_Init(void)
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
//GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出,供USART1的TX使用
//USART初始化
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率为9600。 写完之后,这个Init函数内部会自动算好9600对应的分频系数,然后写入到BRR寄存器
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,无流控。这个参数取值一般以参数名称为前缀
USART_InitStructure.USART_Mode = USART_Mode_Tx; //模式,选择为发送模式。 如果既需要发送模式,又需要接收模式,那就用或|,符号把TX和RX或起来
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,无校验。 No是无校验,Odd奇校验,Even偶校验
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
//USART使能
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte) //调用这个函数,可以从TX引脚发送一个字节数据
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成。 使用USART_FLAG_TXE,发送数据空标志位。 要等待TXE置1,套一个while循环,如TXE标志位==RESET,就一直循环,直到SET,结束等待。
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:串口发送一个数组
* 参 数:Array 要发送数组的首地址
* 参 数:Length 要发送数组的长度
* 返 回 值:无
*/
void Serial_SendArray(uint8_t *Array, uint16_t Length) //这是一个uint8_t *指针类型,指向待发送数组的首地址,由于数组无法判断是否结束,所以需要再传递一个Length进来。
{
uint16_t i; //定义变量i
for (i = 0; i < Length; i ++) //遍历数组。 for循环就会执行Length次,可以对Array数据进行遍历。
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据。 就是依次取出数组Array[i]的每一项,然后通过SendByte发送。
}
}
/**
* 函 数:串口发送一个字符串
* 参 数:String 要发送字符串的首地址
* 返 回 值:无
*/
void Serial_SendString(char *String)
{
uint8_t i; //定义变量
for (i = 0; String[i] != '\0'; i ++) //遍历字符数组(字符串),遇到字符串结束标志位后停止。 这里循环条件,用结束标志位来判断,这里0,是空字符,是字符串结束标志位,如不等于0,还没结束,继续循环,如果等于0,循环停止。这里0可以写成字符的形式'\0',这是空字符的转义字符的表现形式。
{
Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:次方函数(内部使用) //总结是,取某一位,对数字/10的x次方%10
* 返 回 值:返回值等于X的Y次方
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; //设置结果初值为1
while (Y --) //执行Y次
{
Result *= X; //Result累乘Y到次X。就是X的Y次方
}
return Result;
}
/**
* 函 数:串口发送数字
* 参 数:Number 要发送的数字,范围:0~4294967295
* 参 数:Length 要发送数字的长度,范围:0~10
* 返 回 值:无
*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++) //根据数字长度遍历数字的每一位
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Serial_SendByte发送每位数字。 需要把Number的个位,十位,百位,以十进制拆开,然后转换成字符数字对应的数据,依次发送。 '0'是以字符形式
}
//总结是,取某一位,对数字/10的x次方%10
}
/**
* 函 数:使用printf需要重定向的底层函数
* 参 数:保持原始格式即可,无需变动
* 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f) //这是fputc函数原型
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
//重定向fputc跟printf的关系,是这个fputc是printf的底层,printf函数在不断打印的时候,就是不断调用fputc函数一个个打印,我们把fputc函数重定向到串口,那printf自然就输出到串口了。
/**
* 函 数:自己封装的prinf函数
* 参 数:format 格式化字符串
* 参 数:... 用来接收后面的可变的参数列表
* 返 回 值:无
*/
void Serial_Printf(char *format, ...) //函数用来接收格式化字符串
{
char String[100]; //定义字符串
va_list arg; //定义一个参数列表变量,va_list类型名,arg变量名。 定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //String是打印位置,format格式化字符串,arg是参数列表。在这里使用vsprintf,因为sprintf只能接收直接写的参数,对于封装格式用vsprintf。
va_end(arg); //结束变量arg
Serial_SendString(String); //串口发送String
}
Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);
#endif
串口发送+接收
对于串口接收来说,可以使用查询和中断两种方法。如果使用查询初始化就结束了,如使用中断,则需开启中断,配置NVIC。
查询的流程是,在主函数里不判断RXNE标志位,如置1,就收到数据 ,那再调用ReceiveData,读取DR寄存器,这样就行了。
在串口助手,我们需要在发送取区写入数据,发送模式可以选择HEX模式或文本模式。HEX就是原始数据,文本模式首先过一遍字符编码,我们选择HEX模式,在HEX模式下,只能写16进制数,就是0~9,A ~F,不用写0x的。两个数为一组,非法字符都将被忽略。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData; //定义用于接收串口数据的变量
int main(void)
{
//模块初始化
OLED_Init(); //OLED初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "RxData:");
/*串口初始化*/
Serial_Init();
while (1)
{
if (Serial_GetRxFlag() == 1) //检查串口接收数据的标志位
{
RxData = Serial_GetRxData(); //获取串口接收的数据
Serial_SendByte(RxData); //串口将收到的数据回传回去,用于测试
OLED_ShowHexNum(1, 8, RxData, 2); //显示串口接收的数据
}
}
}
Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
uint8_t Serial_RxData; //定义串口接收的数据变量。 封装了
uint8_t Serial_RxFlag; //定义串口接收的标志位变量。 封装了
/**
* 函 数:串口初始化
* 参 数:无
* 返 回 值:无
*/
void Serial_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择。同时开启发送和接收
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断。 开启RXNE标志位到NVIC的输出
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/ //初始化NVIC的USART1的通道
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:串口发送一个数组
* 参 数:Array 要发送数组的首地址
* 参 数:Length 要发送数组的长度
* 返 回 值:无
*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
/**
* 函 数:获取串口接收标志位
* 参 数:无
* 返 回 值:串口接收标志位,范围:0~1,接收到数据后,标志位置1,读取后标志位自动清零
*/
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1) //如果标志位为1
{
Serial_RxFlag = 0; //则返回1,并自动清零标志位
return 1;
}
return 0; //如果标志位为0,则返回0
}
/**
* 函 数:获取串口接收的数据
* 参 数:无
* 返 回 值:接收的数据,范围:0~255
*/
uint8_t Serial_GetRxData(void)
{
return Serial_RxData; //返回接收的数据变量
}
/**
* 函 数:USART1中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
Serial_RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Serial_RxFlag = 1; //置接收标志位变量为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位
}
}
Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);
uint8_t Serial_GetRxFlag(void);
uint8_t Serial_GetRxData(void);
#endif