🚀write in front🚀
🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝💬本系列哔哩哔哩江科大STM32的视频为主以及自己的总结梳理📚
🚀Projeet source code🚀
💾工程代码放在了本人的Gitee仓库:iPickCan (iPickCan) - Gitee.com
引用:
STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili
Keil5 MDK版 下载与安装教程(STM32单片机编程软件)_mdk528-CSDN博客
STM32之Keil5 MDK的安装与下载_keil5下载程序到单片机stm32-CSDN博客
0. 江协科技/江科大-STM32入门教程-各章节详细笔记-查阅传送门-STM32标准库开发_江协科技stm32笔记-CSDN博客
【STM32】江科大STM32学习笔记汇总(已完结)_stm32江科大笔记-CSDN博客
江科大STM32学习笔记(上)_stm32博客-CSDN博客
STM32学习笔记一(基于标准库学习)_电平输出推免-CSDN博客
STM32 MCU学习资源-CSDN博客
stm32学习笔记-作者: Vera工程师养成记
stem32江科大自学笔记-CSDN博客
术语:
英文缩写 | 描述 |
GPIO:General Purpose Input Onuput | 通用输入输出 |
AFIO:Alternate Function Input Output | 复用输入输出 |
AO:Analog Output | 模拟输出 |
DO:Digital Output | 数字输出 |
内部时钟源 CK_INT:Clock Internal | 内部时钟源 |
外部时钟源 ETR:External Trigger | 时钟源 External 触发 |
外部时钟源 ETR:External Trigger mode 1 | 外部时钟源 External 触发 时钟模式1 |
外部时钟源 ETR:External Trigger mode 2 | 外部时钟源 External 触发 时钟模式2 |
外部时钟源 ITRx:Internal Trigger inputs | 外部时钟源,ITRx (Internal trigger inputs)内部触发输入 |
外部时钟源 TIx:exTernal Input pin | 外部时钟源 TIx (external input pin)外部输入引脚 |
CCR:Capture/Comapre Register | 捕获/比较寄存器 |
OC:Output Compare | 输出比较 |
IC:Input Capture | 输入捕获 |
TI1FP1:TI1 Filter Polarity 1 | Extern Input 1 Filter Polarity 1,外部输入1滤波极性1 |
TI1FP2:TI1 Filter Polarity 2 | Extern Input 1 Filter Polarity 2,外部输入1滤波极性2 |
DMA:Direct Memory Access | 直接存储器存取 |
正文:
0. 概述
从 2024/06/12 定下计划开始学习下江协科技STM32课程,接下来将会按照哔站上江协科技STM32的教学视频来学习入门STM32 开发,本文是视频教程 P2 STM32简介一讲的笔记。
本节我们来写一下串口收发数据包的代码
1.🚚第一个代码:串口收发HEX数据包
接线图
和上节的接线基本一样,只是在PB1口接了一个按键,用于控制。
我们在上节的Serial.c文件里加上HEX收发数据包的部分,定义的格式就和上一篇博客中说的HEX数据包接收固定包长,含包头包尾的这个思路一样,就按这个写状态机的思路写。
为了收发数据包,我们先定义两个缓冲区的数组。
这四个数据只存储发送或接收的载荷数据,包头包尾就不存了。
初始化的代码都不需要更改,把上节的这个函数删掉
然后我们先写一个send packet的函数。我们想要的效果是调用一下这个函数,Tx packet数组的四个数据就会自动加上包头报尾发送出去。
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArry(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
实验结果:
接下来我们就来写一下接收数据包的代码
首先在接收中断函数里,我们就需要用状态机来执行接收逻辑了,这就按上面那个状态转移图来写。
首先我们要定义一个标志当前状态的变量s,在中断这里,我们可以在函数里面定义一个静态变量RxState当成这个状态变量s
然后根RxState的不同,需要进入不同的处理程序。
📤️📤️注意:这里一定是要用else if,如果只用3个并列的If,可能在状态转移的时候会出现问题,比如在状态0,你想转移的状态1就置RxState等于1,结果就会造成下面状态1的条件就立马满足了,这样会出现连续两个if都同时成立的情况,这个情况我们不希望出现。
🎵🎵🎵
所以这里使用else if,保证每次进来之后,只能选择执行其中一个状态的代码,或者你用switch case语句,也可以保证只有一个条件满足。
这就是状态选择的部分。然后就依次写每个状态执行的操作和状态转移条件就行了。
🔊🔊🔊这个程序还隐藏有一个问题,就是这个Serial_RxPacket数组,它是一个同时被写入,又同时被读出的数组。
🔊🔊🔊在中断函数里,我们会依次写入它,在主函数里,我们又会依次读出它。这会造成数据包之间可能会混在一起。比如读出的过程太慢了,前面两个数据刚读出来,等了一会儿才继续往后读取。这时后面的数据就有可能会刷新为下一个数据包的数据,也就是读出的数据可能一部分属于上一个数据包,另一部分属于下一个数据包。
🔋🔋解决方法可以在接收部分加入判断,就是在每个数据包读取处理完毕后,再接收下一个数据包。当然,很多情况下其实还可以不进行处理,像这种hex数据包多是用于传输各种传感器的每个独立数据,比如陀螺仪的xyz轴数据,温湿度数据等等,它们相邻数据包之间的数据具有连续性,这样即使相邻数据包混在一起了,也没关系。所以这种情况下就不需要关心这个问题,具体到底怎么处理,还需要大家结合实际情况来操作了,这里就提一下这个可能存在的问题。大家了解一下就行了。
我们这个收发hex数据包的程序大概就讲完了。
接下来加上按键部分的代码,现象是按一下按键变换一下数据,发送到串口助手上。
定义一个变量
然后在主函数中实现按键的控制就行
UART.c
#include "stm32f10x.h" // Device header
#include "Uart.h"
#include <stdio.h>
#include <stdarg.h>
void UART_Init(void)
{
//RCC使能外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //RCC使能UART时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //RCC使能GPIOA时钟
//配置GPIO
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //GPIO为复用推挽输出模式
gpioInitStructure.GPIO_Pin = GPIO_Pin_9; //GPIOA_Pin9为UART1_Tx
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
gpioInitStructure.GPIO_Mode = GPIO_Mode_IPU; //GPIO为上拉输入
gpioInitStructure.GPIO_Pin = GPIO_Pin_10; //GPIOA_Pin9为UART1_Rx
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
//配置UART
USART_InitTypeDef USART_InitStruct;
USART_StructInit(&USART_InitStruct);
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不使用UART硬件流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //UART使能Tx发送
USART_InitStruct.USART_Parity = USART_Parity_No; //UART无奇偶校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //1位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStruct);
//使能UART中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//NVIC配置UART中断优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; //使能UART1_IRQ中断
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
//使能UART
USART_Cmd(USART1, ENABLE);
}
void Serial_SendByte(uint8_t data)
{
USART_SendData(USART1, data);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待UART TXE标志位
//写入之后等待TXE标志位置1,表示数据从TDR寄存器转移到了发送移位寄存器
//如果不等待TDR数据移动到发送移位寄存器,下次写时就会覆盖上一次数据
//该标志位在写DR寄存器时自动清除
}
void Serial_SendArry(uint8_t data[], uint8_t length)
{
for(int i=0; i<length; i++)
{
Serial_SendByte(data[i]);
}
}
void Serial_SendString(char *str)
{
if(str)
{
while(*str != '\0')
{
Serial_SendByte(*str++);
}
}
}
uint32_t Serial_Power(uint32_t x, uint32_t y)
{
uint32_t ret = 1;
while(y > 0)
{
ret *= x;
y--;
}
return ret;
}
void Serial_SendNumber(uint32_t number, uint16_t length)
{
int i = 0;
uint8_t data;
for(i=0; i<length; i++)
{
data = number/Serial_Power(10, length - i -1)%10 + '0';
Serial_SendByte(data);
}
}
/* 重写fputc()函数,重定向printf()到串口
*
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...)
{
char buf[128];
va_list arg;
va_start(arg, format);
vsprintf(buf, format, arg);
va_end(arg);
Serial_SendString(buf);
}
volatile uint8_t Seial_RxFlag = 0;
uint8_t Serial_RxPacket[4];
uint8_t Serial_TxPacket[4];
//中断服务函数
void USART1_IRQHandler()
{
static uint8_t Uart_State;
static uint8_t pRxPacket = 0;;
uint8_t Seial_Data = 0;
Seial_Data = USART_ReceiveData(USART1);
if(Uart_State == 0)
{
if(Seial_Data == 0xFF)
{
Uart_State = 1;
}
}
else if(Uart_State == 1)
{
Serial_RxPacket[pRxPacket] = Seial_Data;
pRxPacket++;
if(pRxPacket >= 4)
{
pRxPacket = 0;
Uart_State = 2;
}
}
else if(Uart_State == 2)
{
if(Seial_Data == 0xFE)
{
Uart_State = 0;
Seial_RxFlag = 1;
}
}
//清除UART RXNE 标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
uint8_t Serial_GetRxFlag(void)
{
uint8_t ret = 0;
if(Seial_RxFlag)
{
ret = 1;
Seial_RxFlag = 0;
}
return ret;
}
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArry(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
man.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
extern uint16_t Num;
int main(int argc, char *argv[])
{
OLED_Init();
OLED_ShowString(1, 1, "UART:");
OLED_ShowString(1, 1, " ");
UART_Init();
//printf("Hello World\r\n");
//OLED_ShowString(2, 1, "RX:");
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
Serial_SendPacket();
while(1)
{
if(Serial_GetRxFlag() == 1)
{
OLED_ShowHexNum(1, 1, Serial_RxPacket[0], 2);
OLED_ShowHexNum(1, 4, Serial_RxPacket[1], 2);
OLED_ShowHexNum(1, 7, Serial_RxPacket[2], 2);
OLED_ShowHexNum(1, 11, Serial_RxPacket[3], 2);
}
}
return 1;
}
实验现象:
实验改进2,按下按键收发送数据:
main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
extern uint16_t Num;
int main(int argc, char *argv[])
{
OLED_Init();
Key_Init();
OLED_ShowString(1, 1, "UART:");
OLED_ShowString(1, 1, " ");
UART_Init();
//printf("Hello World\r\n");
//OLED_ShowString(2, 1, "RX:");
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
Serial_SendPacket();
uint8_t Key_Num = 0;
while(1)
{
Key_Num = Key_GetNum();
if(Key_Num == 1)
{
Serial_TxPacket[0]++;
Serial_TxPacket[1]++;
Serial_TxPacket[2]++;
Serial_TxPacket[3]++;
OLED_ShowString(1, 1, "Tx Packet:");
OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);
OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
OLED_ShowHexNum(2, 11, Serial_TxPacket[3], 2);
}
if(Serial_GetRxFlag() == 1)
{
OLED_ShowString(3, 1, "Rx Packet:");
OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);
OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
OLED_ShowHexNum(4, 11, Serial_RxPacket[3], 2);
}
}
return 1;
}
实验结果:
2🚚.第二个程序:串口收发文本数据包
接线图:
在PA1口接了一个LED,用于指示。
我们在上一个工程的基础上改,按键部分的代码就不要了。
接下来就按上节讲过的文本数据包接收的思路来写,这里是可变包长,含包头包尾。
我们这里就只写接收的部分,因为发送的话不方便像hex数组一样一个个更改的。所以发送就直接在主函数里sendstring或者printf就行了,非常简单。
所以这个发送数据包的函数就不要了
接收部分我们来来实现一下。
数组的长度给多点,防止溢出,给个100,这要求单条指令最长不能超过一百个字符。
之后是中断的状态机部分,参考上面状态转移图写。
接下来就把LED部分的代码加进来就行
先是判断字符串是不是等于我们规定的指令再执行相应的操作。判断字符串要用到字符串处理函数,要包含头文件#include "string.h"。
在这里我们判断两个字符串是否相等,需要用到strcmp函数,如果不知道这个函数的用法可以去翻一下我的C语言复习专栏,在这篇博文中我详细介绍了字符串处理函数。
🔊🔊还有个问题需要说明,同样还是之前的个问题。如果连续发送数据包程序处理不及时,可能导致数据包错位。
🔊🔊在这里,文本数据包,每个数据包是独立的,不存在连续,这如果错位了问题就比较大。所以在程序这里我们可以修改一下,等每次处理完成之后,再开始接收下一个数据包。
我们可以这样在中断函数中等待包头的时候再加一个条件,如果数据等于包头并且Serial_RxFlag等于等于0才执行接收,否则就是发的太快了,还没处理完,就跳过这个数据包。
然后上面这个读取标志位之后立刻清零的函数先删掉。
运行结果
UART.c
#include "stm32f10x.h" // Device header
#include "Uart.h"
#include <stdio.h>
#include <stdarg.h>
void UART_Init(void)
{
//RCC使能外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //RCC使能UART时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //RCC使能GPIOA时钟
//配置GPIO
GPIO_InitTypeDef gpioInitStructure;
gpioInitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //GPIO为复用推挽输出模式
gpioInitStructure.GPIO_Pin = GPIO_Pin_9; //GPIOA_Pin9为UART1_Tx
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
gpioInitStructure.GPIO_Mode = GPIO_Mode_IPU; //GPIO为上拉输入
gpioInitStructure.GPIO_Pin = GPIO_Pin_10; //GPIOA_Pin9为UART1_Rx
gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpioInitStructure);
//配置UART
USART_InitTypeDef USART_InitStruct;
USART_StructInit(&USART_InitStruct);
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不使用UART硬件流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //UART使能Tx发送
USART_InitStruct.USART_Parity = USART_Parity_No; //UART无奇偶校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //1位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStruct);
//使能UART中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//NVIC配置UART中断优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; //使能UART1_IRQ中断
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
//使能UART
USART_Cmd(USART1, ENABLE);
}
void Serial_SendByte(uint8_t data)
{
USART_SendData(USART1, data);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待UART TXE标志位
//写入之后等待TXE标志位置1,表示数据从TDR寄存器转移到了发送移位寄存器
//如果不等待TDR数据移动到发送移位寄存器,下次写时就会覆盖上一次数据
//该标志位在写DR寄存器时自动清除
}
void Serial_SendArry(uint8_t data[], uint8_t length)
{
for(int i=0; i<length; i++)
{
Serial_SendByte(data[i]);
}
}
void Serial_SendString(char *str)
{
if(str)
{
while(*str != '\0')
{
Serial_SendByte(*str++);
}
}
}
uint32_t Serial_Power(uint32_t x, uint32_t y)
{
uint32_t ret = 1;
while(y > 0)
{
ret *= x;
y--;
}
return ret;
}
void Serial_SendNumber(uint32_t number, uint16_t length)
{
int i = 0;
uint8_t data;
for(i=0; i<length; i++)
{
data = number/Serial_Power(10, length - i -1)%10 + '0';
Serial_SendByte(data);
}
}
/* 重写fputc()函数,重定向printf()到串口
*
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...)
{
char buf[128];
va_list arg;
va_start(arg, format);
vsprintf(buf, format, arg);
va_end(arg);
Serial_SendString(buf);
}
volatile uint8_t Seial_RxFlag = 0;
uint8_t Serial_RxPacket[128];
uint8_t Serial_TxPacket[4];
//中断服务函数
void USART1_IRQHandler()
{
static uint8_t Uart_State = 0;
static uint8_t pRxPacket = 0;;
uint8_t Seial_Data = 0;
Seial_Data = USART_ReceiveData(USART1);
if(Uart_State == 0)
{
if(Seial_Data == '@')
{
Uart_State = 1;
}
}
else if(Uart_State == 1)
{
if(Seial_Data == '\r')
{
Uart_State = 2;
Serial_RxPacket[pRxPacket] = '\0';
pRxPacket = 0;
}
else{
Serial_RxPacket[pRxPacket] = Seial_Data;
pRxPacket++;
}
}
else if(Uart_State == 2)
{
if(Seial_Data == '\n')
{
Uart_State = 0;
Seial_RxFlag = 1;
}
}
//清除UART RXNE 标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
uint8_t Serial_GetRxFlag(void)
{
uint8_t ret = 0;
if(Seial_RxFlag)
{
ret = 1;
Seial_RxFlag = 0;
}
return ret;
}
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArry(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"
#include "UART.h"
#include <stdio.h>
#include "Key.h"
#include "String.h"
#include "LED.h"
extern uint16_t Num;
int main(int argc, char *argv[])
{
OLED_Init();
Key_Init();
LED_Init();
OLED_ShowString(1, 1, "Rx:");
UART_Init();
//Serial_SendPacket();
uint8_t Key_Num = 0;
Serial_SendString("UART TEST\r\n");
while(1)
{
if(Serial_GetRxFlag() == 1)
{
OLED_ShowString(2, 1, (char *)Serial_RxPacket);
if(strcmp((char *)Serial_RxPacket, "LED_ON") == 0)
{
OLED_ShowString(3, 1, " ");
OLED_ShowString(3, 1, "LED_ON_OK");
Serial_SendString("LED_ON_OK");
LED1_On();
}
else if(strcmp((char *)Serial_RxPacket, "LED_OFF") == 0)
{
OLED_ShowString(3, 1, " ");
OLED_ShowString(3, 1, "LED_OFF_OK");
Serial_SendString("LED_OFF_OK");
LED1_Off();
}
else
{
OLED_ShowString(3, 1, " ");
OLED_ShowString(3, 1, "UN_SUPPORT_CMD");
Serial_SendString("UN_SUPPORT_CMD");
}
}
}
return 1;
}
这就是我们第二个程序的现象。
本节的内容到这里就结束了,下节继续。