链接:https://pan.baidu.com/s/1eFhcnmv37iYll_UsWgx8Rw?pwd=8888
提取码:8888
自学stm32以来 坑踩了不少,今天分享一下自己的学习笔记
硬件部分 usb转ttl串口一个 stm32f103c8t6一个 杜邦线若干 0.96 4针 oled一个 stlinkv2一个
软件部分 Keil5 串口助手任意 要是没有链接里有
本文推荐阅读人群:刚刚入门stm32的人 标准库学习stm32的人
特别鸣谢 江科大自动协(b站)
正文
串口通信是最基本最简单的通信方式,笔者曾经做过OpenMv识别数字的题目,通过串口发送将识别到的数据传输到小车主控里面,不过调的都是大佬写好的串口收发模块,直到今天才算理解透彻了串口到底是怎么样发送与接受的。
tx与rx是怎样收发的呢 高低电平,如何产生高低电平是硬件的事情,本文略过,基于标准库操作寄存器去完成程序才是本文的要点。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
//PB 1和12是按键 你要实在没有按键,额 搞一根杜邦线 一端接PB1一端接GND
///*引脚配置*/
//#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)(x))
//#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)(x))
//特别声明 此代码出自江科大自动协 本代码更改仅做学习笔记使用 兜先生于20230625
//正文;请优先看Serial.c里的内容
//#include "LED.h"
特别注意 没有独立按键就算了,插根杜邦线也可以,一端接地,另一端拿手里,想什么时候插就插进去然后拔出来,软件会扫描的。
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
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, ...);
void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);
#endif
这是串口1的头文件,
#ifndef __SERIAL_H
#define __SERIAL_H
#endif
这三句话是标准的头文件写法 如果害怕代码里有重名可以这么写,然后其他的语句都是函数的声明部分,主函数头文件里只需要#include “seril.h”就可以把所有函数在主函数里面使用了
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//串口1的时钟在APB2总线上 先挂载时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//标准使能A口总线时钟
GPIO_InitTypeDef GPIO_InitStructure; //GPIO 的结构体声明 可以引出后续的几个参数 Mode Pin Speed
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);//将赋值完的三个参数 赋给结构体然后传递给GPIO初始化函数
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);
USART_InitTypeDef USART_InitStructure; //串口初始化结构体 选模式与GPIO差不多
USART_InitStructure.USART_BaudRate = 9600;//波特率 实际上另一种写法是
//申请一个参数 直接将参数赋给它 然后主函数初始化 usart_init(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字节 基本上大家都是1字节 当然看你想要啥
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位字长
USART_Init(USART1, &USART_InitStructure);//赋值
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//这个是中断法接受数据 一般来讲程序比较小你用查表法也可以
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//优先级分组分两组 00 01 10 11
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
串口初始化函数,串口收发信息有两种方式一种是查表法,也就是主函数循环里不断查询寄存器的标志位,如果标志位变化了,那么进行xxxx操作,另一种就是中断了,不影响浪费系统时钟,客户已去做其他事然后有数据再管传进来的数据,很方便,学透彻了对以后的操作系统学习有好处。
NVIC是管理分组的,有优先级的区分,同优先级看处理优先级,同处理只能看接口号了,或者说谁来的早,谁先,中断嵌套的具体知识大家肯定清楚。
实际上对c8t6来说只有三个串口PA9 10 2 3 PB 10 11 学会一个其他就差不太多了
void Serial_SendByte(uint8_t Byte) //单字节 也就是1byte
{
USART_SendData(USART1, Byte);//发送一个字节
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//监听发送完成
}
//发送一个数组 数组的长度由你给定 当然也可以不给定 写一个不定字符串就行 没啥大区别就是需要循环判断 发送
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]);
}
}
其实上边的发送一个字节是最关键的,剩下的什么字符串啊,数组啊,说到底不也是循环使用发送一个字节,那我们着重说一下发送一个字节,发送一个字节,当寄存器发送完成会物理也就是硬件置1 那么我们判断发送完成就去监听这个1就OK了,while循环一下。等他置1 跳出循环。
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
//注释人 兜先生 错误疏漏恳请指正 stlinkv2编译
//
uint8_t Serial_TxPacket[4]; //FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//串口1的时钟在APB2总线上 先挂载时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//标准使能A口总线时钟
GPIO_InitTypeDef GPIO_InitStructure; //GPIO 的结构体声明 可以引出后续的几个参数 Mode Pin Speed
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);//将赋值完的三个参数 赋给结构体然后传递给GPIO初始化函数
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);
USART_InitTypeDef USART_InitStructure; //串口初始化结构体 选模式与GPIO差不多
USART_InitStructure.USART_BaudRate = 9600;//波特率 实际上另一种写法是
//申请一个参数 直接将参数赋给它 然后主函数初始化 usart_init(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字节 基本上大家都是1字节 当然看你想要啥
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位字长
USART_Init(USART1, &USART_InitStructure);//赋值
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//这个是中断法接受数据 一般来讲程序比较小你用查表法也可以
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//优先级分组分两组 00 01 10 11
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
//实际上你要想改 这个函数随便改 USART_SendData这个才是最底层的函数
//然后对具体的硬件寄存器来讲 你只要发送完成我也将发送完成的标志位1了随便你怎么写
void Serial_SendByte(uint8_t Byte) //单字节 也就是1byte
{
USART_SendData(USART1, Byte);//发送一个字节
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//监听发送完成
}
//发送一个数组 数组的长度由你给定 当然也可以不给定 写一个不定字符串就行 没啥大区别就是需要循环判断 发送
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]);
}
}
//这是对 数字进行按位取出的操作 比如1234%10=4;/10再取余 一样的
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;//1234 就是100*10+20*10+3*10+0*10+4
}
//发送一个十进制的 数据
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');//数是从左往右看 所以是大数先出
}
}
//重定向printf 要选择USE MIcolib选项 然后引入stdio.h
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
//重定向sprintf
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);
}
//按照标准数据帧发送 例如 你在发送区写 ff12345678fe 其他也可以 看你想要什么就改什么 改数组长度也行 别忘记修改括号里的4就行
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);//帧头
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);//帧尾
}
//转移标志位
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
void USART1_IRQHandler(void)//
//串口一中断服务函数 其目的是将按照首个接受字节为0xff字长大于4的有效数据转存至数组中
//后续可对这个数组值进行操作,这个串口传来的可以是任意符合标准的 串口数据 是串口通信的重要组成部分
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)//监听标志位
{
uint8_t RxData = USART_ReceiveData(USART1);//将单字节 转存用以判断传输数据开始
if (RxState == 0)//0 1 2 三种状态 用以防止数据传输错误或者编程思路失误
{
if (RxData == 0xFF)//如果接受的首个数据是 按照你的传输协议来的数据那么进入状态二存储
{
RxState = 1;
pRxPacket = 0;//每次调用到这里的时候会对下一个所用的数组的可变变量清零 防止疏漏
}
}
else if (RxState == 1)//状态一
{
Serial_RxPacket[pRxPacket] = RxData;//存储
pRxPacket ++;
if (pRxPacket >= 4)//一般来讲等于4就会触发 但是为了防止意外还是大于等于比较好
{
RxState = 2;
}
}
else if (RxState == 2)
{
if (RxData == 0xFE)
{
RxState = 0;//状态标志位清零
Serial_RxFlag = 1;//此时 一个接受过程完成 转到uint8_t Serial_GetRxFlag(void)函数进行 标志位的转移
//接上,不过私以为直接申请全局变量这边置1 主函数直接就接到数据更方便
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);//相对应的寄存器标志位清零
}
}
我一次性全发了吧,想要直接移植的.c .h都有了 快去试试吧,想要继续了解的可以去B站搜索江科大自动协。
感谢您的观看