Start中是启动文件,是STM32中最基本的文件,不需要修改,添加即可。
启动文件包含很多类型,要根据芯片型号来进行选择:
如果是选择超值系列,那就使用带 VL 的启动文件,若是普通版就选择不带VL的
然后再根据Flash的大小,选择LD(lower density),MD,HD或者XL
stm32f10x.h 是STM32的外设寄存器描述文件,是用来描述STM32有哪些寄存器和它对应地址的
system文件 是用来配置时钟的
由于STM32是由内核和内核外围的设备组成的,并且内核的寄存器描述(CoreSupport
)和外围的寄存器描述文件(DeviceSupport
)不是在一起的,因此还需添加“内核寄存器”的描述文件。
两个CM3 是内核的寄存器描述
当project中没有引入Library时,即无库函数,使用编程是通过直接操作寄存器来进行的。因此需要引入STM32的库函数。
1.引入头文件和源文件
STM32F10x_StdPeriph_Driver 为STM32标准外设驱动文件夹,其中
inc 是库函数的头文件
src 是库函数的源文件,在其中:misc 是内核的库函数,其他的是内核外的外设库函数
将这两个文件夹下的头文件和源文件全部复制到,项目文件夹下的Library中。
keil5 中将文件加入工程。
2.引入配置文件
STM32F10x_StdPeriph_Template 是一个示例项目文件夹,其中
stm32f10x_conf.h (configuration) 文件是用来配置库函数头文件的包含关系的,还有用于参数检查的函数定义,是所有库函数都需要的。
两个以 it 结尾的文件 是用来存放中断函数的。
将这3个文件复制到项目的User中。
keil5 中将文件加入工程。
4.Keil软件配置
打开主函数所引入的第一个头文件 stm32f10x.h, 在偏向最下方的部分找到
#ifdef USE_STDPERIPH_DRIVER
#include "stm32f10x_conf.h"
#endif
这是一个条件编译,表示:如果定义了 USE_STDPERIPH_DRIVER 语句(使用标准外设驱动),其中的引入头函数语句才有效。
因此,复制语句USE_STDPERIPH_DRIVER
到 keil5 软件的魔法棒 → C/C++ → Define:。
再把下方的头文件路径中加入 Library和user。
经过以上两步操作,引入了启动文件和库函数文件,分别放在 Start 和 Library 之中,配置完成后,这两个文件夹中的文件都是不需要修改的(人家给的权限也是只读)。
需要修改的只有 User 文件夹下的代码。
GPIOC 是指单片机(如 STM32、Arduino 等)中的一个 GPIO 端口,其中 GPIO 是 General Purpose Input Output(通用输入输出)的缩写,而 C 表示这个端口属于 C 组。
在单片机中,GPIO 可以被用来控制数字信号的输入和输出。例如,可以将 GPIOC 配置为输出模式,在程序中控制它的高低电平,从而控制外部设备的状态或执行某些操作。另外,GPIOC 也可以被配置为输入模式,从而读取外部设备发送的数字信号。
数据输入(从右向左看):
由此,信号便进入了输入数据寄存器,再用程序读取寄存器中的数据,便可以获得端口的输入电平了。
也可以输入到片上外设,分别有模拟输入和复用功能输入。
数据输出(从左往右看):
数据输出的来源有两种:输出数据寄存器和片上外设。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
初始化时,第二个参数是一个结构体:
GPIO_InitTypeDef GPIO_InitStruct;
其中需要设置3个属性值,分别是 GPIO_Mode
(工作模式)、GPIO_Pin
(引脚选择)、GPIO_Speed
(输出速度)
GPIO_Mode
的参数有以下8个,分别对应上一小节说的GPIO的8种工作模式{ GPIO_Mode_AIN = 0x0, //模拟输入
GPIO_Mode_IN_FLOATING = 0x04, //浮空输入
GPIO_Mode_IPD = 0x28, //下拉输入
GPIO_Mode_IPU = 0x48, //上拉输入
GPIO_Mode_Out_OD = 0x14, //开漏输出
GPIO_Mode_Out_PP = 0x10, //推挽输出
GPIO_Mode_AF_OD = 0x1C, //复用开漏
GPIO_Mode_AF_PP = 0x18 //复用推挽
}GPIOMode_TypeDef;
GPIO_Pin
是使用宏定义来进行命名的,直接复制变量名作为属性值。当使用多个引脚时,可使用或运算|
(由下面的定义可以看出,不同的引脚分别在不同的二进制位处为1,或运算,可将其进行选择)GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_13
#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
GPIO_Speed
一般情况下选择50MHz即可{
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;
综上,下面是一个初始化的例子:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); // 初始化GPIOC的外设时钟,若使用的是PB或PA就改成GPIOB,GPIA
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //工作模式设置为推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; //初始化引脚PA13,一般在STM32板子上是自带的LED
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //输出速度选择为50MHz
GPIO_Init(GPIOC, &GPIO_InitStruct);
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //可把指定的端口设置为高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//可把指定的端口设置为低电平
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); // 可同时对16个端口i进行写入操作
gpio_writebit() 是用于设置一个 GPIO 引脚的值,只能将其设置为 0 或 1。
gpio_setbits() 和 gpio_resetbits() 则可以同时设置多个引脚的状态。 gpio_setbits() 可以将指定的引脚设置为 1 ,而 gpio_resetbits() 可以将它们设置为 0 。因此,这两个函数比 gpio_writebit() 更适合同时控制多个 GPIO 引脚(按位或 )的情况。
具体细节不记录了,看视频。
总结:TX引脚输出定时翻转的高低电平,RX引脚定时读取引脚的高低电平。每个字节的数据加上起始位、停止位、可选的校验位打包为数据帧,一次输出在TX引脚,另一端RX引脚依次接收,这样就完成了字节数据的传递。
在此引入库函数小节,我们知道Library
文件夹存放了STM32的库函数,而库函数就是在寄存器操作层面向上抽象了一层,提供了丰富的硬件进行操作的函数接口。
但在主函数中,我们很多情况下只想要简单明确的函数来进行一个想要的操作。比如在使用串口,我在写主函数时,只想简单地,我给出数据,它负责传输。不想去管具体我还要初始化什么时钟,使用哪个端口,配置什么乱七八糟的设置。因此,我们就需要在库函数和主函数之间再架起一个桥梁,使用其将这些使用库函数操作硬件的细节封装起来,向上对主函数提供简单易用的新函数接口。
因此,我们在库函数层面上再向上抽象一层,就是所谓的驱动。在此,我们新建一个文件夹Hardware
,用来存放这些硬件驱动(eg:按键驱动,LED驱动),以及即将编写的串口驱动。
在Hardware文件夹中新建Serial.c
和Serial.h
文件,Hardware文件夹是用来存放硬件驱动(eg:按键驱动,LED驱动),与Library文件夹就是使用库函数来实现我们自己所需的逻辑,
首先查看串口的库函数中带有哪些函数,打开Library/stm32f10x_usart.c
,发现有许许多多的函数,但我们在写驱动时,需要用到的只有其中常用的就够了,比如:
下面是具体的代码:
下面这些函数就是我们将库函数封装起来,向上抽象给主函数的方便易用的新函数接口,是可以在主函数中直接调用的。
串口的初始化封装为函数Serial_Init
,有如下步骤
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
(STM32引脚定义中,PA9不仅可以作为GPIO口,也可以复用作为 USART1_TX 即串口输出 来使用)
配置成复用推挽输出,将PA10PA10复用为 USART1_RX 即串口接收输入 来使用
配置成上拉输入。GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
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;
USART_InitStructure.USART_BaudRate = 9600; //波特率:9600
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不使用流控
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //串口模式:TX发送、接收双模式
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);
startup_stm32f10x_md.s
中找到——USART1_IRQHandler
。USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组???
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);
Serial_SendByte
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); \\直接调用库函数SendData来将Byte写入TDR
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); \\等待标志位,避免数据覆盖
}
Serial_SendArray
\*
* Array : 数组指针
* Length :数组长度
*\
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]); //依次取出数组Array的每一项,利用Serial_SendByte进行发送
}
}
Serial_SendString
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++) //字符串会以'\0'结束
{
Serial_SendByte(String[i]);
}
}
Serial_SendNumber
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');
}
}
fputc
(为什么直接在c文件中定义了,就改变了库中的printf函数?此处也没有使用类似于java的重写override操作)int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
对于串口接收来说,可以使用查询和中断两种方法。
ReceiveData
,读取DR寄存器。while(1){
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET){ //判断RXNE标志位
RXData = USART_ReceiveData(USART1); //接收一个字节,放在变量RXData中
OLED_ShowHexNum(1, 1, RXData, 2); //若硬件连有OLED显示屏,可以显示在其上
}
}
USART1_IRQHandler
来接收数据。void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断RXNE标志位
{
Serial_RxData = USART_ReceiveData(USART1); //Serial_RxData 为定义在该文件中的全局变量,用来暂存数据
Serial_RxFlag = 1; //Serial_RxFlag 为定义在该文件中的全局变量,用来暂存标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //以防没读取DR时,不能自动清除标志位,此处咱们手动清除标志位
}
}
为了向上封装完全,此处还提供两个函数,分别供主函数来判断是否接收完毕,以及获取暂存在驱动文件中的Serial_RxData数据
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "RxData:");
Serial_Init();
//串口发送
Serial_SendByte(0x41);
uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
Serial_SendArray(MyArray, 4);
Serial_SendString("\r\nNum1=");
Serial_SendNumber(111, 3);
printf("\r\nNum2=%d", 222);
char String[100];
sprintf(String, "\r\nNum3=%d", 333);
Serial_SendString(String);
Serial_Printf("\r\nNum4=%d", 444);
Serial_Printf("\r\n");
while (1)
{
//串口接收
if (Serial_GetRxFlag() == 1) //判断标志位
{
RxData = Serial_GetRxData(); //接收一个字节的数据
Serial_SendByte(RxData); //回传显示
OLED_ShowHexNum(1, 8, RxData, 2);
}
}
}
通过在包头和包尾指定专用的标志数据,来划分不同的包。
问题:当载荷数据与与包头包尾重复,该怎么办?
- 限制载荷数据的范围,使得包头包尾不在数据范围中
- 使用固定长度的数据包,这样便可以使用包头包尾来进行数据对齐
数据对齐:在接收载荷数据位置的数据时,并不会判断是包头包尾。但在接收包头包尾位置的数据时,会判断它是否确实是包头、包尾。- 增加包头包尾的数量,使其呈现出载荷数据出现不了的情况
优点:传输最直接,解析数据简单,适合模块发送原始的数据。比如使用串口通信的陀螺仪,温湿度传感器
缺点:灵活性不足,载荷容易和包头包尾重复
在文本数据包中,每个字节经过了一层编码和译码
优点:数据直观易理解,适合人机交互的场合输入指令。比如蓝牙模块使用的AT指令,CNC和3D打印机常用的G代码。
缺点:解析效率低下
Serial_SendPacket
Serial.c
中的全局变量Serial_TxPacket,如果要在主函数中使用的化。可以使用get、set函数来传递指针,或者直接在Serial.h
文件中使用 extern
声明出去,因为嵌入式编程很多情况下没那么高的设计要求,因此很多使用到全局变量和这种不利于软件工程设计理念的方式)void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
USART1_IRQHandler
Serial_RxPacket
接收数组中。static
静态变量类似全局变量,只会在函数第一次进入时进行初始化,函数退出后,数据仍然有效,但与全局变量不同的是,静态变量只能在本函数使用。)uint8_t Serial_RxPacket[4]; // 接收缓冲区
uint8_t Serial_RxFlag; //接收标志位
void USART1_IRQHandler(void)
{
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)
{
if (RxData == 0xFF)
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
if (pRxPacket >= 4)
{
RxState = 2;
}
}
else if (RxState == 2)
{
if (RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1; //接收完成
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
发送文本数据包
可以使用Serial_SendString
或者重定向printf
接收文本数据包
此处,为了防止主函数和硬件处理速度不匹配,导致出现数据包错位,所以要在Serial.h
将extern uint8_t Serial_RxFlag;
也声明出去,使得主函数和Serial.c
都可以读写该变量,以便于主函数处理完成这个数据包之后,Serial.c
才进行下一个数据包的接收。
这样在主函数中,if (Serial_RxFlag == 1)
表示接收到数据包,主函数开始处理;在处理完成之后,Serial_RxFlag = 0;
告诉Serial.c
的接收部分,主函数已处理完毕,可以进行下一个数据包的接收了。
char Serial_RxPacket[100]; //接收缓冲区
void USART1_IRQHandler(void)
{
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)
{
if (RxData == '@' && Serial_RxFlag == 0) // 遇到包头且主函数处理完毕上一个数据包
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
if (RxData == '\r') //接收时遇到第一个包尾'\r'
{
RxState = 2;
}
else
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
}
}
else if (RxState == 2)
{
if (RxData == '\n')
{
RxState = 0;
Serial_RxPacket[pRxPacket] = '\0'; // 给字符串加入结束字符'\0'
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}