基于STM32实现OTA&BootLoader 第二章——外设功能开发

参考教程: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
}
相关推荐
2501_918126912 小时前
stm32能刷什么程序?
linux·stm32·单片机·嵌入式硬件·学习
国科安芯2 小时前
ASP4644S电源芯片引脚功能与参考设计输出电压计算方法
网络·单片机·嵌入式硬件·fpga开发·性能优化
国科安芯2 小时前
抗辐照MCU芯片在核工业水下探测耐辐照数字摄像机中的应用研究
网络·单片机·嵌入式硬件
Zevalin爱灰灰3 小时前
基于STM32实现OTA&BootLoader 第一章——概述
stm32·单片机·物联网·嵌入式
TDengine (老段)3 小时前
TDengine IDMP 高级功能——计量单位
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
一路往蓝-Anbo3 小时前
第 10 章:OpenAMP 实战——构建 M33 与 Linux 的 RPMsg 消息隧道
linux·运维·服务器·驱动开发·stm32·单片机·嵌入式硬件
FakeOccupational3 小时前
【电路笔记 STM32】STM32CubeMX配置&自动移植FreeRTOS + STM32&FreeRTOS点灯的最简单示例
笔记·stm32·单片机
2501_918126914 小时前
stm32什么程序效率最高,体积小,运行快,适应广?
c语言·stm32·单片机·嵌入式硬件·学习
2501_918126914 小时前
stm32能做次声波发射器吗?
linux·stm32·嵌入式硬件·学习·个人开发