参考教程:https://www.bilibili.com/video/BV1SatHeBEVG/?spm_id_from=333.1387.favlist.content.click
一、准备工作
1、软件层的准备(Keil工程)
(1)拷贝一份STM32教程中使用OLED屏进行显示的工程文件夹,并更名为"OTA&BootLoader实验工程"。

(2)打开工程,将模拟I2C、模拟SPI、串口模块、DMA模块、W25Q64模块和FLASH模块的分文件添加到工程中(这几个分文件在STM32教程中已经完成开发),并编译。24C02模块(使用I2C通信的外设EEPROM)未在其它教程中开发过,因此需要从零开发。

2、硬件层的准备(接线)
(1)USB转串口模块:
|---------|------------|
| STM32引脚 | 模块引脚 |
| PA10 | TXD |
| PA9 | RXD |
| GND | GND |
| / | VCC与3.3V短接 |
(2)W25Q64模块:
|---------|------|
| STM32引脚 | 模块引脚 |
| PA4 | CS |
| PA5 | CLK |
| PA6 | DO |
| PA7 | DI |
| 3.3V | VCC |
| GND | GND |
(3)24C02模块:
|---------|------|
| STM32引脚 | 模块引脚 |
| PB11 | SDA |
| PB10 | SCL |
| 3.3V | VCC |
| GND | GND |
二、串口&DMA功能开发
1、处理接收数据方案设计
(1)如果使用CPU进行接收数据的处理,对于简单的项目而言问题不但,但对于复杂的项目,CPU往往有"更重要的任务"需要处理,如果接收数据全程由CPU进行算力支持,是非常奢侈的,而DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,能够节省CPU的资源,因此数据传输可采用DMA。

(2)涉及数据传输,还需要判断单次数据传输是否完成,最简单的就是判断传输信号线是否空闲,如果空闲,说明一次数据传输结束,可以根据此逻辑配置中断,用于请求CPU发出数据处理指令。

(3)单片机需定义缓冲区(一般为一维数组)用于存放接收但未处理的数据,如果单片机的数据处理效率高于数据接收速率,那么缓冲区的大小可相应地较小,为方便介绍,以下以一个2048 bytes的缓冲区为例进行介绍。

①当有源源不断的数据到来时,缓冲区数组从0号下标开始存放数据,先使用标号较小的空间,按顺序往缓冲区中填入数据。

②当缓冲区被使用的标号超过2047后,继续增大标号会引发数组越界,因此需要约定好单次传递数据的最大字节数,比如412 bytes,当新的一组数据到来时,如果缓冲区剩余空间小于412 bytes,那么新的数据应该从缓冲区0号下标位置写入,以规避数据越界风险。当然,也可以采用传统的循环队列,不管高地址的剩余空间有多少,都按标号递增的策略写入数据,直到标号到达2047,再转到0标号继续写入数据,这种策略的开发逻辑会变得稍微复杂一点,且意义不大,并没有起到节约空间的作用。

③缓冲区中只是存放了若干组连续的数据,但并没法直接区分不同组数据的头尾,因此在将数据写入缓冲区时,还应该记录下数据组的起始地址Start和结束地址End,为了方便管理,可以将这些信息定义为一个结构体,再创建结构体数组,当结构体数组溢出时,可按照循环队列的思想从首元素重新写入。

④由于缓冲区是个写入与读出同时不断进行的结构,因此需要再设置IN指针和OUT指针指示缓冲区写入和读出的位置,以便DMA进行数据处理。IN指针和OUT指针可直接指向结构体数组,通过结构体成员间接索引缓冲区的数据,IN指针指向下一个可写入数据的位置,OUT指针指向下一个DMA需要读取的数据的位置,整体思想与循环队列是极其相似的。(可再定义END指针指示结构体数组末端,下图未示出)

2、Serial.h文件修改
在Serial.h文件中添加如下代码,主要是缓冲区相关的定义
cpp
#define U0_RX_SIZE 2048 //接收数据缓冲区大小(单位为字节)
#define U0_RX_MAX 256 //单次接收最大数据大小(单位为字节)
#define NUM 10 //数据组指针结构体数组大小
extern uint8_t U0_RxBuff[U0_RX_SIZE]; //接收数据缓冲区
//数据组指针结构体
typedef struct{
uint8_t *start; //单组数据起始地址
uint8_t *end; //单组数据结束地址
}UCB_URxBuffptr;
//数据组指针结构体数组结构体
typedef struct{
uint16_t URxCounter; //接收数据个数
UCB_URxBuffptr URxDataPtr[NUM]; //数据组指针结构体数组
UCB_URxBuffptr *URxDataIN; //IN指针
UCB_URxBuffptr *URxDataOUT; //OUT指针
UCB_URxBuffptr *URxDataEND; //END指针
}UCB_CB;
3、串口初始化函数修改
(1)首先在Serial.c文件中包含Serial.h文件,否则无法使用其中的定义。
(2)核对串口初始化函数Serial_Init。
cpp
void Serial_Init(void){
//开启GPIO和USART的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//USART1_TX(PA9)配置为复用输出模式,USART1_RX(PA10)配置为输入模式
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);
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);
//配置USART1
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);
//开启中断用于接收数据,配置NVIC
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //RXNE位置为1,也就是RDR中有新数据,会触发一次中断(不使用中断会消耗很多软件资源)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组方式2
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //USART1到NVIC的通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //开启中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //响应优先级
NVIC_Init(&NVIC_InitStructure);
//开启USART1
USART_Cmd(USART1, ENABLE);
}
4、DMA初始化函数修改
(1)DMA的数据转运方向配置为串口外设数据寄存器至接收数据缓存区。

(2)单次数据传输是否结束是由另外的中断判断,不依赖DMA的传输计数器,因此将传输计数器设置为大于单次数据传输允许的数据大小最大值。
(3)针对以上两点,核对DMA初始化函数MyDMA_Init。
cpp
void MyDMA_Init(uint16_t Size)
{
MyDMA_Size = Size; //记录传入的Size值(一轮转运的数据个数/传输计数器初值)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; //外设基地址,设置为USART1的数据寄存器地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //每次转运1个字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //起点的地址不自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)U0_RxBuff; //终点的基地址,设置为接收数据缓冲区数组首地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //每次转运1个字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //终点的地址自增
DMA_InitStructure.DMA_BufferSize = U0_RX_MAX+1; //传输计数器初值,设置为大于单次传输最大值
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //单次模式(计数器自减为0,一轮转运结束)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设站点(DMA_PeripheralBaseAddr)作为数据源(方向参数)
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //使用软件触发(DMA开启后,只要传输计数器的值不为0,转运会一直进行)
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //指定优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
//DMA1_Channel1:选择DMA1的通道1进行AddrA->AddrB的数据转运
}
5、接收数据缓冲区初始化函数编写
(1)首先在Serial.c文件中定义接收数据缓冲区以及数据组指针结构体数组结构体。
cpp
uint8_t U0_RxBuff[U0_RX_SIZE]; //接收数据缓冲区
UCB_CB U0CB; //数据组指针结构体数组结构体
(2)IN指针和OUT指针初始化为指向结构体数组的首个成员的位置,END指针初始化为指向结构体数组最后一个成员的位置,该初始化函数也定义在Serial.c文件中。
cpp
void U0Rx_PtrInit(void)
{
U0CB.URxDataIN = &U0CB.URxDataPtr[0]; //IN指针指向结构体数组首个成员的位置
U0CB.URxDataOUT = &U0CB.URxDataPtr[0];//OUT指针指向结构体数组首个成员的位置
U0CB.URxDataEND = &U0CB.URxDataPtr[NUM-1]; //END指针指向结构体数组最后一个成员的位置
U0CB.URxDataIN->start = U0_RxBuff;
U0CB.URxCounter = 0; //结构体数组中的"有效成员"数量为0
}
