STM32

STM32是STMicroelectronics(意法半导体)推出的一系列基于ARM Cortex-M内核的32位微控制器(MCU)产品。这些微控制器提供了广泛的产品系列,覆盖了多种不同的性能和功能需求,适用于各种应用领域,包括工业控制、汽车电子、消费类电子、医疗设备等。

STM32系列微控制器以其高性能、低功耗、丰富的外设接口和灵活的开发工具而闻名。它们通常具有丰富的存储器、多种通信接口(如UART、SPI、I2C、CAN等)、模拟数字转换器(ADC)、定时器、PWM输出等功能,以满足不同应用场景下的需求。

STM32微控制器通常使用标准的ARM Cortex-M内核,包括Cortex-M0、M0+、M3、M4和M7等,这些内核具有不同的性能和功耗特性,可根据具体应用的需求进行选择。此外,STM32系列还提供了多种封装和引脚配置,以满足不同尺寸和集成度的要求。

STMicroelectronics为STM32系列提供了丰富的开发工具和支持资源,包括基于ARM开发环境的集成开发环境(IDE)、调试器、评估板和参考设计等。这些工具和资源有助于开发人员快速开发和部署他们的应用,并提供了全面的技术支持和文档资料,帮助用户充分发挥STM32微控制器的性能和功能优势。

前言

UART是重要的片上资源,主流单片机基本上都有该功能,通过UART可以扩展出很多的通信接口,如RS232、RS485、LIN,甚至WIFI、蓝牙模组等,可以说只要搞通讯就会涉及到UART。下面和大家分享STM32的UART配置。

1、UART是什么

USART全称universal synchronous asynchronous receiver transmitter通用同步异步接收发送器;速率最高可达4.5Mbits/s,波特率460800;

数据按位顺序发送的串行通信接口简称串口,USART模块是采用串行通信接口最常见的模块,为了方便,就把USART简称为串口;

USART接口通过RX,TX,GND同其他设备相连;当TX引脚被禁止时,该引脚恢复GPIO的配置;当TX引脚使能且未发送数据时,该引脚处于高电平(空闲态);

USART接口的数据字长度可编程,停止位长度可编程;可配置为DMA多缓冲通信;

2、USART的帧格式

串口数据应该遵循USART帧的格式,才能被串口识别;

首先总线需要持续至少一个空闲帧,然后连续发送数据帧,数据帧与数据帧之间有时会有断开帧,断开帧后需要接1-2bit停止位,连接下个数据帧;

断开帧只能为10bit或11bit低电平的帧(CR1_SBK[0]);然后接1或2bit的高电平作为停止位,然后接下一个数据帧;

数据帧的数据字有两种格式,(1)8 bit 数据位;(2)8bit 数据位 + 1 bit 奇偶校验位;

“STM32单片机,UART的寄存器配置以及工作原理"

3、USART的寄存器使用

每个USART都有7个自己的寄存器;用来配置该USART的所有功能;

有许多功能诸如硬件流控制,LIN模式,智能卡模式等,由于没用过或是用不上,实在晦涩难懂费时费力,故在此全部跳过;

以下给出了USART作为常用串口收发数据的工作框图,以及相关的寄存器配置;

3.1 工作框图

“STM32单片机,UART的寄存器配置以及工作原理"

“STM32单片机,UART的寄存器配置以及工作原理"

3.2 相关寄存器配置

1)首先需要配置USART的6个参数:

波特率USART_BRR,字长M,停止位STOP,校验位PCE,PS,PEIE,USART的收发模式TE和RE和硬件流控制CTSIE,CTSE,RTSE;

2)USART提供了8个中断:TXEIE, TCIE, RXNEIE, PEIE, IDLEIE, CTSIE, LBDIE, EIE;8个中断使能均可以进入USART的中断函数,根据需要配置合适的中断使能位为1;通常为RXNEIE位;

3)然后使能接收器RE和发送器TE;

4)然后使能UE中断;

“STM32单片机,UART的寄存器配置以及工作原理"

4、USART的代码示例

4.1 标准库提供的常用USART接口

标准库为所有的外设都提供了封装寄存器的API接口函数,文件名为stm32f10x_peripheral.c;

以下为usart外设的常用函数;

//串口USARTx的参数配置初始化函数;
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
//使能串口,(主要是分频器和输出的设置)
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
//使能串口中断,(就是那8个中断,均可以进入中断函数)
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
//都是处理一个字节;
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
//读取SR寄存器的状态,SR的状态都是硬件设置的;
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
//读取SR寄存器和CRx控制寄存器的状态,和上面一个功能相同的;
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);

//修改SR寄存器的状态,单功能通讯用不上;
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);

4.2 USART1使用代码

#include "usartDemo.h"  
u8 USART1_RX_BUF[256];    //接收缓存
u8 USART1_RX_CNT = 0;    //接收字节计数
u8 USART1_REV_0D = 0;    //收到\r
u8 USART1_REV_0A = 0;    //收到\r和\n

//usart1初始化之后,便可以通过串口读写了;
void Usart1_Init(u32 bound)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //设置NVIC中断分组2:2位抢占优先级,2位响应优先级   0-3;

    //USART1外设中断配置
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;    //抢占优先级3
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;            //子优先级3
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                //IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);  

    //GPIO初始化 USART1_TX    PA9
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    //输出需要配置速率,输入不需要配置速率;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;        //复用推挽输出,<中文..手册>8.1.11外设的GPIO配置
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    //GPIO初始化    USART1_RX    PA10
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;        //浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);

   //USART1初始化
    USART_InitStructure.USART_BaudRate = bound;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;   //CR1中的TE,RE  
    USART_Init(USART1, &USART_InitStructure); 

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//CR1中的RXNEIE中断
    USART_Cmd(USART1, ENABLE);                    //CR1中的UE
}

void USART1_Send_Data(u8 *buf,u16 len)
{
    u16 t;
    for(t=0;t<len;t++)        
    {
        USART_SendData(USART1,buf[t]);
        while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET); 
        //发送字节完成后,TC硬件置1;
    }    //先读SR,后写DR清除TC位;
    USART1_RX_CNT = 0;
    USART1_REV_0D = 0;
    USART1_REV_0A = 0;  
}

void USART1_IRQHandler(void)                    
{
    u8 Res;
    if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
     {
         Res =USART_ReceiveData(USART1);        //读DR,硬件清0 RXNE位;
        USART1_RX_BUF[USART1_RX_CNT]=Res;    //接收数据  
        USART1_RX_CNT++; 
        if(Res==0x0d)
            USART1_REV_0D = 1;
        if(USART1_REV_0D&&(Res==0x0a))
            USART1_REV_0A = 1;                      
     }
    //RXNE为1,读数据的同时又来了数据,那么新的数据丢失;产生溢出错误,读完数据后RXNE为0,但ORE标志还在;
    //RXNE为1,又来了数据,产生接收溢出错误,置位ORE;
    if(USART_GetFlagStatus(USART1,USART_FLAG_ORE) == SET)
    {
        USART_ReceiveData(USART1);
    //    USART_ClearFlag(USART1,USART_FLAG_ORE);//先读SR,后读DR,可以复位ORE位;应该不用软件清除了;
    }
    // USART_ClearFlag(USART1,USART_IT_RXNE); //读DR可以清除RXNE,应该不用软件清除了;
}

int main(void)
{
    Usart1_Init(460800);
    while(1)
    {
        if(USART1_REV_0A)
        {
            USART1_Send_Data(USART1_RX_BUF,USART1_RX_CNT);
        }
    }    
}

4.2.1 在前面代码的基础上不使用串口中断,直接通过SR状态位来判断数据的收发;

将上面代码的usart1初始化代码中CR1的RXNEIE配置行注释掉,然后修改main函数如下即可;

int main(void)
{
    Usart1_Init(460800); 
    while(1)
    {
        if ((USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET))
        {
            USART1_RX_BUF[USART1_RX_CNT] = USART_ReceiveData(USART1);
            if(USART1_RX_BUF[USART1_RX_CNT]==0x0a)
                USART1_REV_0A = 1;
            USART1_RX_CNT++;
        }
        if(USART1_REV_0A)
        {
            USART1_REV_0A = 0;
            for(int i=0;i<USART1_RX_CNT;i++)
            {
                USART_SendData(USART1, USART1_RX_BUF[i]);
                while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET);
            }
            USART1_RX_CNT=0;
        }
    }
}

5、总结

USART的功能没想到还挺多的,寄存器看起来就有些费时了,很多概念都是新的,不好理解,直接拉低了效率;于是觉得这样不行,应该用什么看什么,用到再看,学海无涯,精力有限;

另外人家费心费力写好标准库不就是为了帮开发人员省时间吗?了解一下即可,以后没必要深入;

本文代码github:https://github.com/caesura-k/stm32f1_usart

来源:玩转嵌入式
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 811

1、概述

说明

每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。

STM32作为一款高端 Cortex-M3系列单片机,有必要了解它的启动文件。打好基础,为以后优化程序,写出高质量的代码最准备。

本文以一个实际测试代码--START_TEST为例进行阐述。

整体过程

STM32整个启动过程是指从上电开始,一直到运行到 main函数之间的这段过程,步骤为(以使用微库为例):

①上电后硬件设置SP、PC

②设置系统时钟

③软件设置SP

④加载.data、.bss,并初始化栈区

⑤跳转到C文件的main函数

代码

启动过程涉及的文件不仅包含 startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件 entry.o、entry2.o、entry5.o、entry7.o等(从生成的 map文件可以看出来)。

2、程序在Flash上的存储结构

在真正讲解启动过程之前,先要讲解程序下载到 Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。

“了解STM32启动过程,好优化程序"

上图中:

  • MSP初始值由编译器生成,是主堆栈的初始值。

  • 初始化数据段是.data

  • 未初始化数据段是.bss

.data和.bss是在__main里进行初始化的,对于ARM Compiler,__main主要执行以下函数:

“了解STM32启动过程,好优化程序"

其中__scatterload会对.data和.bss进行初始化。

加载数据段和初始化栈的参数

加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。

0x0800033c  Flash上的数据段(初始化数据段和未初始化数据段)起始地址
0x20000000  加载到SRAM上的目的地址
0x0000000c  数据段的总大小
0x080002f4  调用函数_scatterload_copy

需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。

“了解STM32启动过程,好优化程序"

3、数据在SRAM上的结构

程序运行时(执行到main函数)时的SRAM数据结构

“了解STM32启动过程,好优化程序"

4、详细过程分析

有了以上的基础,现在详细分析启动过程。

上电后硬件设置SP、PC

刚上电复位后,硬件会自动根据向量表偏移地址找到向量表,向量表偏移地址的定义如下:

“了解STM32启动过程,好优化程序"

调试现象如下:

“了解STM32启动过程,好优化程序"

看看我们的向量表内容(通过J-Flash打开hex文件)

“了解STM32启动过程,好优化程序"

硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成复位,结果为:

SP = 0x02000810
PC = 0x08000145

设置系统时钟

上一步中令 PC=0x08000145的地址没有对齐,硬件自动对齐到 0x08000144,执行 SystemInit函数初始化系统时钟。

软件设置SP

LDR   R0,=__main
  BX   R0

执行上两条之类,跳转到 __main程序段运行,注意不是main函数, ___main的地址是0x0800 0130。

可以看到指令LDR.W sp,[pc,#12],结果SP=0x2000 0810。

加载.data、.bss,并初始化栈区

BL.W     __scatterload_rt2 

进入 __scatterload_rt2代码段。

__scatterload_rt2:
0x080001684C06      LDR      r4,[pc,#24]  ; @0x08000184
0x0800016A4D07      LDR      r5,[pc,#28]  ; @0x08000188
0x0800016C E006      B        0x0800017C
0x0800016E68E0      LDR      r0,[r4,#0x0C]
0x08000170 F0400301  ORR      r3,r0,#0x01
0x08000174 E8940007  LDM      r4,{r0-r2}
0x080001784798      BLX      r3
0x0800017A3410      ADDS     r4,r4,#0x10
0x0800017C42AC      CMP      r4,r5
0x0800017E D3F6      BCC      0x0800016E
0x08000180 F7FFFFDA  BL.W     _main_init (0x08000138)

这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:

0x0800016E68E0      LDR      r0,[r4,#0x0C]
0x08000170 F0400301  ORR      r3,r0,#0x01
0x08000174
0x080001784798      BLX      r3

当然执行这两个函数的时候,还需要传入参数。至于参数,我们在“加载数据段和初始化栈的参数”环节已经阐述过了。当这两个函数都执行完后,结果就是“数据在SRAM上的结构”所展示的图。最后,也把事实加载和初始化的两个函数代码奉上如下:

                 __scatterload_copy:
0x080002F4 E002      B        0x080002FC
0x080002F6 C808      LDM      r0!,{r3}
0x080002F81F12      SUBS     r2,r2,#4
0x080002FA C108      STM      r1!,{r3}
0x080002FC2A00      CMP      r2,#0x00
0x080002FE D1FA      BNE      0x080002F6
0x080003004770      BX       lr
                 __scatterload_null:
0x080003024770      BX       lr
                 __scatterload_zeroinit:
0x080003042000      MOVS     r0,#0x00
0x08000306 E001      B        0x0800030C
0x08000308 C101      STM      r1!,{r0}
0x0800030A1F12      SUBS     r2,r2,#4
0x0800030C2A00      CMP      r2,#0x00
0x0800030E D1FB      BNE      0x08000308
0x080003104770      BX       lr

跳转到C文件的main函数

                 _main_init:
0x080001384800      LDR      r0,[pc,#0]  ; @0x0800013C
0x0800013A4700      BX       r0

5、异常向量与中断向量表

; VectorTableMapped to Address0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size


__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler; ResetHandler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler; HardFaultHandler
                DCD     MemManage_Handler; MPU FaultHandler
                DCD     BusFault_Handler; BusFaultHandler
                DCD     UsageFault_Handler; UsageFaultHandler
                DCD     0; Reserved
                DCD     0; Reserved
                DCD     0; Reserved
                DCD     0; Reserved
                DCD     SVC_Handler                ; SVCallHandler
                DCD     DebugMon_Handler; DebugMonitorHandler
                DCD     0; Reserved
                DCD     PendSV_Handler; PendSVHandler
                DCD     SysTick_Handler; SysTickHandler


; ExternalInterrupts
                DCD     WWDG_IRQHandler            ; WindowWatchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
                DCD     FLASH_IRQHandler           ; Flash
                DCD     RCC_IRQHandler             ; RCC
                DCD     EXTI0_IRQHandler           ; EXTI Line0
                DCD     EXTI1_IRQHandler           ; EXTI Line1
                DCD     EXTI2_IRQHandler           ; EXTI Line2
                DCD     EXTI3_IRQHandler           ; EXTI Line3
                DCD     EXTI4_IRQHandler           ; EXTI Line4
                DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel1
                DCD     DMA1_Channel2_IRQHandler   ; DMA1 Channel2
                DCD     DMA1_Channel3_IRQHandler   ; DMA1 Channel3
                DCD     DMA1_Channel4_IRQHandler   ; DMA1 Channel4
                DCD     DMA1_Channel5_IRQHandler   ; DMA1 Channel5
                DCD     DMA1_Channel6_IRQHandler   ; DMA1 Channel6
                DCD     DMA1_Channel7_IRQHandler   ; DMA1 Channel7
                DCD     ADC1_2_IRQHandler          ; ADC1 & ADC2
                DCD     USB_HP_CAN1_TX_IRQHandler  ; USB HighPriority or CAN1 TX
                DCD     USB_LP_CAN1_RX0_IRQHandler ; USB LowPriority or CAN1 RX0
                DCD     CAN1_RX1_IRQHandler        ; CAN1 RX1
                DCD     CAN1_SCE_IRQHandler        ; CAN1 SCE
                DCD     EXTI9_5_IRQHandler         ; EXTI Line9..5
                DCD     TIM1_BRK_IRQHandler        ; TIM1 Break
                DCD     TIM1_UP_IRQHandler         ; TIM1 Update
                DCD     TIM1_TRG_COM_IRQHandler    ; TIM1 Trigger and Commutation
                DCD     TIM1_CC_IRQHandler         ; TIM1 CaptureCompare
                DCD     TIM2_IRQHandler            ; TIM2
                DCD     TIM3_IRQHandler            ; TIM3
                DCD     TIM4_IRQHandler            ; TIM4
                DCD     I2C1_EV_IRQHandler         ; I2C1 Event
                DCD     I2C1_ER_IRQHandler         ; I2C1 Error
                DCD     I2C2_EV_IRQHandler         ; I2C2 Event
                DCD     I2C2_ER_IRQHandler         ; I2C2 Error
                DCD     SPI1_IRQHandler            ; SPI1
                DCD     SPI2_IRQHandler            ; SPI2
                DCD     USART1_IRQHandler          ; USART1
                DCD     USART2_IRQHandler          ; USART2
                DCD     USART3_IRQHandler          ; USART3
                DCD     EXTI15_10_IRQHandler       ; EXTI Line15..10
                DCD     RTCAlarm_IRQHandler; RTC Alarm through EXTI Line
                DCD     USBWakeUp_IRQHandler; USB Wakeup from suspend
                DCD     TIM8_BRK_IRQHandler        ; TIM8 Break
                DCD     TIM8_UP_IRQHandler         ; TIM8 Update
                DCD     TIM8_TRG_COM_IRQHandler    ; TIM8 Trigger and Commutation
                DCD     TIM8_CC_IRQHandler         ; TIM8 CaptureCompare
                DCD     ADC3_IRQHandler            ; ADC3
                DCD     FSMC_IRQHandler            ; FSMC
                DCD     SDIO_IRQHandler            ; SDIO
                DCD     TIM5_IRQHandler            ; TIM5
                DCD     SPI3_IRQHandler            ; SPI3
                DCD     UART4_IRQHandler           ; UART4
                DCD     UART5_IRQHandler           ; UART5
                DCD     TIM6_IRQHandler            ; TIM6
                DCD     TIM7_IRQHandler            ; TIM7
                DCD     DMA2_Channel1_IRQHandler   ; DMA2 Channel1
                DCD     DMA2_Channel2_IRQHandler   ; DMA2 Channel2
                DCD     DMA2_Channel3_IRQHandler   ; DMA2 Channel3
                DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4& Channel5
__Vectors_End

这段代码就是定义异常向量表,在之前有一个“J-Flash打开hex文件”的图片跟这个表格是一一对应的。编译器根据我们定义的函数 Reset_Handler、NMI_Handler等,在连接程序阶段将这个向量表填入这些函数的地址。

startup_stm32f10x_hd.s内容:


NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP




stm32f10x_it.c中内容:
void NMI_Handler(void)
{
}

在启动汇编文件中已经定义了函数 NMI_Handler,但是使用了“弱”,它允许我们再重新定义一个 NMI_Handler函数,程序在编译的时候会将汇编文件中的弱函数“覆盖掉”--两个函数的代码在连接后都存在,只是在中断向量表中的地址填入的是我们重新定义函数的地址。

6、使用微库与不使用微库的区别

“了解STM32启动过程,好优化程序"

使用微库就意味着我们不想使用MDK提供的库函数,而想用自己定义的库函数,比如说printf函数。那么这一点是怎样实现的呢?我们以printf函数为例进行说明。

不使用微库而使用系统库

在连接程序时,肯定会把系统中包含printf函数的库拿来调用参与连接,即代码段有系统库的参与。

在启动过程中,不使用微库而使用系统库在初始化栈的时候,还需要初始化堆(猜测系统库需要用到堆),而使用微库则是不需要的。

    IF      :DEF:__MICROLIB


                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit


                 ELSE


                 IMPORT  __use_two_region_memory
                 EXPORT  __user_initial_stackheap


__user_initial_stackheap


                 LDR     R0, =  Heap_Mem
                 LDR     R1, =(Stack_Mem+ Stack_Size)
                 LDR     R2, = (Heap_Mem+  Heap_Size)
                 LDR     R3, = Stack_Mem
                 BX      LR


                 ALIGN


                 ENDIF

另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:

                  __main:
0x08000130 F000F802  BL.W     __scatterload_rt2_thumb_only (0x08000138)
0x08000134 F000F83C  BL.W     __rt_entry_sh (0x080001B0)
                 __scatterload_rt2_thumb_only:
0x08000138 A00A      ADR      r0,{pc}+4; @0x08000164
0x0800013A E8900C00  LDM      r0,{r10-r11}
0x0800013E4482      ADD      r10,r10,r0
0x080001404483      ADD      r11,r11,r0
0x08000142 F1AA0701  SUB      r7,r10,#0x01
                 __scatterload_null:
0x0800014645DA      CMP      r10,r11
0x08000148 D101      BNE      0x0800014E
0x0800014A F000F831  BL.W     __rt_entry_sh (0x080001B0)
0x0800014E F2AF0E09  ADR.W    lr,{pc}-0x07; @0x08000147
0x08000152 E8BA000F  LDM      r10!,{r0-r3}
0x08000156 F0130F01  TST      r3,#0x01
0x0800015A BF18      IT       NE
0x0800015C1AFB      SUBNE    r3,r7,r3
0x0800015E F0430301  ORR      r3,r3,#0x01
0x080001624718      BX       r3
0x080001640298      LSLS     r0,r3,#10
0x080001660000      MOVS     r0,r0
0x0800016802B8      LSLS     r0,r7,#10
0x0800016A0000      MOVS     r0,r0
                 __scatterload_copy:
0x0800016C3A10      SUBS     r2,r2,#0x10
0x0800016E BF24      ITT      CS
0x08000170 C878      LDMCS    r0!,{r3-r6}
0x08000172 C178      STMCS    r1!,{r3-r6}
0x08000174 D8FA      BHI      __scatterload_copy (0x0800016C)
0x080001760752      LSLS     r2,r2,#29
0x08000178 BF24      ITT      CS
0x0800017A C830      LDMCS    r0!,{r4-r5}
0x0800017C C130      STMCS    r1!,{r4-r5}
0x0800017E BF44      ITT      MI
0x080001806804      LDRMI    r4,[r0,#0x00]
0x08000182600C      STRMI    r4,[r1,#0x00]
0x080001844770      BX       lr
0x080001860000      MOVS     r0,r0
                 __scatterload_zeroinit:
0x080001882300      MOVS     r3,#0x00
0x0800018A2400      MOVS     r4,#0x00
0x0800018C2500      MOVS     r5,#0x00
0x0800018E2600      MOVS     r6,#0x00
0x080001903A10      SUBS     r2,r2,#0x10
0x08000192 BF28      IT       CS
0x08000194 C178      STMCS    r1!,{r3-r6}
0x08000196 D8FB      BHI      0x08000190
0x080001980752      LSLS     r2,r2,#29
0x0800019A BF28      IT       CS
0x0800019C C130      STMCS    r1!,{r4-r5}
0x0800019E BF48      IT       MI
0x080001A0600B      STRMI    r3,[r1,#0x00]
0x080001A24770      BX       lr
                 __rt_lib_init:
0x080001A4 B51F      PUSH     {r0-r4,lr}
0x080001A6 F3AF8000  NOP.W
                 __rt_lib_init_user_alloc_1:
0x080001AA BD1F      POP      {r0-r4,pc}
                 __rt_lib_shutdown:
0x080001AC B510      PUSH     {r4,lr}
                 __rt_lib_shutdown_user_alloc_1:
0x080001AE BD10      POP      {r4,pc}
                 __rt_entry_sh:
0x080001B0 F000F82F  BL.W     __user_setup_stackheap (0x08000212)
0x080001B44611      MOV      r1,r2
                 __rt_entry_postsh_1:
0x080001B6 F7FFFFF5  BL.W     __rt_lib_init (0x080001A4)
                 __rt_entry_postli_1:
0x080001BA F000F919  BL.W     main (0x080003F0)

使用微库而不使用系统库

在程序连接时,不会把包含printf函数的库连接到终极目标文件中,而使用我们定义的库。

启动时需要完成的工作就是之前论述的步骤1、2、3、4、5,相比使用系统库,启动过程步骤更少。

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 119

问题:

该问题由某客户提出,发生在STM32F103VDT6器件上。据其工程师讲述:在其产品的设计中,STM32的HSE外接8MHz的晶体产生振荡,然后通过STM32内部的PLL倍频到72MHz,作为STM32的系统时钟,驱动芯片工作。在STM32片外有专用的看门狗芯片,监控STM32的运行。STM32内部的软件会在STM32的某个管脚上产生脉冲来复位看门狗。一旦STM32没有及时的产生脉冲来复位门狗,则看门狗会认为STM32运行不正常,从而复位STM32。在对该产品做可靠性测试时,进行了对看门狗监控时钟失效能力的测试。测试的方法是:将HSE外接的晶体的两个端子接地,使其停止振荡,从而验证看门狗能否做出对STM32的做出复位动作。试验结果表明,看门狗没有产生复位动作。进一步测试发现,STM32在失效情况下仍在向看门狗发送复位脉冲。

调研:

重复测试,确认其所述现象属实。检查软件代码,确认其软件没有开启STM32的CSS功能。修改代码,将PLL的二分频从STM32的MCO管脚送出,以方便用示波器观察。通过控制晶体的管脚是否接地来控制HSE是否振荡。当HSE正常振荡时,MCO送出的信号频率为36MHz,当HSE停止振荡时,MCO送出的信号的频率在1.7MHz附近,如图(一)所示:

“时钟失效之后,STM32还能运行?"

“时钟失效之后,STM32还能运行?"

通过调试器观察寄存器RCC_CFGR中的SWS控制控制位,其值为[10],说明此时的系统时钟确实来自PLL的输出。

从 STM32F103VD 的数据手册中查找PLL相关的参数如表(一):

“时钟失效之后,STM32还能运行?"

其中,PLL的输出频率范围是16MHz–72MHz。也就是说,PLL在处于相位锁定的状态下,可以输出16MHz–72MHz的时钟信号。而当输入信号频率过低而导致输出信号频率低于16MHz时,将可能处于失锁的状态。在这状态下,它的输出信号的频率与输入信号的频率之间,不一定符合所设定的倍频与分频关系。更确切的说,不能通过公式:

“时钟失效之后,STM32还能运行?"

得出“输入信号频率为零时,输出信号频率也为零”这样的结论。这一点与实测的结果相吻合。

结论:

STM32的PLL在没有输入信号的情况下,仍能维持在最低的频点处振荡,产生输出。以至,CPU及其它外设仍能在PLL送出的时钟的驱动下运行。所以,通过判断有无时钟来驱动CPU执行指令的方式来判断HSE是否失效是行不通的。

处理:

对软件做如下修改:

1. 在软件的初始化部分,开启 STM32 的 CSS 功能;

2. 修改 NMI 中断服务程序,加入 while(1) 陷阱语句;

开启 CSS 功能后,当 HSE 失效时,STM32 会自动开启 HSI,并将系统时钟的来源切换到HSI 的输出,同时产生 NMI 中断。这样,程序的流程将停留在 NMI 中而不能产生复位片外的看门狗的脉冲。当片外看门狗溢出后,就会复位 STM32,使其恢复到正常驻的状。

建议:

STM32中的CSS功能是专门为检测和处理HSE失效而设计的。但该功能在STM32复位后是被禁止的,须要软件对其使能才会发挥作用。当CSS单元检测到HSE失效时,它会使能HSI,并将系统时钟切换到HSI。同时,它会关闭HSE,如果PLL的输入信号来自HSE的输出,它也会关闭PLL。CSS单元在做时钟调整的同时,也会产生一个NMI中断请求,和一个送给高级定时器的刹车信号。NMI中断请求会产生一个NMI中断,以便用户程序可以在中断服务程序中做紧急处理,而刹车信号则是使高级定时器进入刹车状态,以防止由其控制的电机驱动桥臂由于失去控制而过流。用户程序可以在NMI中断服务程序中尝试恢复HSE及PLL的功能,也可以使用陷阱让程序的流程停留在服务程序中,从而等待看门狗复位整个系统。

“时钟失效之后,STM32还能运行?"

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 45

STM32单片机应用非常广泛,官方提供了标准的接口库,用户可以不用直接操作寄存器,只需要调用接口函数就可以了。在官方库中有一个非常重要的函数void SystemInit (void),

该函数用户可能不会直接调用,而在启动文件中一定会调用。函数原型如下:

函数原型

void SystemInit (void)
{
  /* Reset the RCC clock configuration to the default reset state(for debug purpose) */
  /* Set HSION bit */
  RCC->CR |= (uint32_t)0x00000001;

  /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */   
  
  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
  RCC->CFGR &= (uint32_t)0xFF80FFFF;

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;      
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
    
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl(); 
  #endif /* DATA_IN_ExtSRAM */
#endif 

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
  SetSysClock();

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif 
}

可以看到函数体中几乎全是条件编译。

(1)先看第一行代码:RCC->CR |= (uint32_t)0x00000001;显然这是给CR寄存器的最低一位赋值为1.官方寄存器配置详解截图:

“STM32库函数SystemInit()详解"

“STM32库函数SystemInit()详解"

编译条件宏定义

#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */   

这个条件编译是根据芯片容量不同默认初始化CFGR寄存器(Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits )。

“STM32库函数SystemInit()详解"
 RCC->CR &= (uint32_t)0xFEF6FFFF;
 RCC->CR &= (uint32_t)0xFFFBFFFF;

显然是把CR寄存器的某些位赋值,其作用为:Reset HSEON, CSSON and PLLON ,HSEBYPbits即将HSEON,CSSON,PLLON,HSEBYP位置为零。

/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;

作用为把CFGR寄存器的PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE位置0。

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;      
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */

这个条件编译块的作用为根据芯片容量初始化中断位(关闭中断)。

#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
  SystemInit_ExtMemCtl(); 
  #endif /* DATA_IN_ExtSRAM */
#endif 

这个条件编译块的作用为初始化Memory控制。

static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
  SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
  SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
  SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
  SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
  SetSysClockTo56();  
#elif defined SYSCLK_FREQ_72MHz
  SetSysClockTo72();
#endif
 
 /* If none of the define above is enabled, the HSI is used as System clock
    source (default after reset) */ 
}

我们可以看到该函数就是通过判断定义了哪个宏定义标志符而调用不同的设置sys时钟频率的函数,官方固件库默认定义了SYSCLK_FREQ_72MHz,所以会调用SetSysClockTo72这个函数。

如果要使用其它频率,那就解开相应注释(只保留一个不被注释)。

SetSysClockTo72()函数如下:

static void SetSysClockTo72(void)
{
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;
  
  /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/    
  /* Enable HSE */    
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);
 
  /* Wait till HSE is ready and if Time out is reached exit */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;
    StartUpCounter++;  
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

  if ((RCC->CR & RCC_CR_HSERDY) != RESET)
  {
    HSEStatus = (uint32_t)0x01;
  }
  else
  {
    HSEStatus = (uint32_t)0x00;
  }  

  if (HSEStatus == (uint32_t)0x01)
  {
    /* Enable Prefetch Buffer */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* Flash 2 wait state */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;    

 
    /* HCLK = SYSCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
      
    /* PCLK2 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
    
    /* PCLK1 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

#ifdef STM32F10X_CL
    /* Configure PLLs ------------------------------------------------------*/
    /* PLL2 configuration: PLL2CLK = (HSE / 5) * 8 = 40 MHz */
    /* PREDIV1 configuration: PREDIV1CLK = PLL2 / 5 = 8 MHz */
        
    RCC->CFGR2 &= (uint32_t)~(RCC_CFGR2_PREDIV2 | RCC_CFGR2_PLL2MUL |
                              RCC_CFGR2_PREDIV1 | RCC_CFGR2_PREDIV1SRC);
    RCC->CFGR2 |= (uint32_t)(RCC_CFGR2_PREDIV2_DIV5 | RCC_CFGR2_PLL2MUL8 |
                             RCC_CFGR2_PREDIV1SRC_PLL2 | RCC_CFGR2_PREDIV1_DIV5);
  
    /* Enable PLL2 */
    RCC->CR |= RCC_CR_PLL2ON;
    /* Wait till PLL2 is ready */
    while((RCC->CR & RCC_CR_PLL2RDY) == 0)
    {
    }
    
   
    /* PLL configuration: PLLCLK = PREDIV1 * 9 = 72 MHz */ 
    RCC->CFGR &= (uint32_t)~(RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL);
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLSRC_PREDIV1 | 
                            RCC_CFGR_PLLMULL9); 
#else    
    /*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
#endif /* STM32F10X_CL */

    /* Enable PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* Wait till PLL is ready */
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }
    
    /* Select PLL as system clock source */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;    

    /* Wait till PLL is used as system clock source */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
    {
    }
  }
  else
  { /* If HSE fails to start-up, the application will have wrong clock 
         configuration. User can add here some code to deal with this error */
  }
}

这个函数体比较长,但仔细看会发现这个函数就是在配置CR,CFGR,ACR(设置FLASH)寄存器的某些位(使能,判断是否就绪,设置相应位,设置FLASH,设置AHB,APB预分频系数,设置HCLK,PCLK等等外设时钟,设置PLL锁相环倍频系数最终确定系统时钟),结合官方注释和官方寄存器的说明很容易理解。

至此,SystemInit函数就能大概理解了。但是还有一个问题需要注意:那就是虽然我们在main函数中并没有调用SystemInit函数,但它在start up启动文件中被调用了:

“STM32库函数SystemInit()详解"

可以看到SystemInit函数是在main函数之前执行的,要是自定义该函数,那这里也要修改名称,建议不要随意修改或者重构该函数。

来源:玩转嵌入式
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 364

IAP很常见了,我这里主要是记录一下我所使用的方法,调试也花了两天时间。我所用的型号是STM32F103C8T6,这个片子估计是目前性价比最高的了,所以平时也都是用的这个。这个IC有64KFlash和20K的RAM,也有小道说有后置隐藏的64K,也就是说其实是有128K,我一直也没有测试,有空测测,有大神这样说,估计是可以的。这里重点记录一下我写的IAP思路和代码以及细节和遇到坑的地方。先大体的概述一下,最后贴上我认为重点的代码。

在概述之前先要解决一个问题,那就是sram空间和flash空间的问题,sram只有20K,flash有64k。

解决的办法有很多:

1)最常见的就是自己写上位机软件,通过分包发送,期间还可以加入加密算法,校验等等。

2)使用环形队列,简单点说就是个环形数组,一边接收上位机数据,一边往flash里面写。

这里条件限制就采用第二种方法。所以即使是分给A和B的25K空间的flash空间,sram只有20K也是不能一次接收完所有的bin数据的,这里我只开辟了一个1K的BUF,使用尾插法写入,我的测试应用程序都在5-6K,用这样的方法可以在9600波特率下测试稳定,也试过57600的勉强可以的,115200就不行了。

环形队列代码如下:

C文件:

#include "fy_looplist.h"
 
#include "fy_includes.h"
 
 
#ifndef NULL
#define NULL 0
#endif
 
#ifndef min
#define min(a, b) (a)<(b)?(a):(b) //< 获取最小值
#endif
 
#define DEBUG_LOOP 1
 
static int Create(_loopList_s* p,unsigned char *buf,unsigned int len);
static void Delete(_loopList_s* p);
static int Get_Capacity(_loopList_s *p);
static int Get_CanRead(_loopList_s *p);
static int Get_CanWrite(_loopList_s *p);
static int Read(_loopList_s *p, void *buf, unsigned int len);
static int Write(_loopList_s *p, const void *buf, unsigned int len);
 
struct _typdef_LoopList  _list=
{
    Create,
    Delete,
    Get_Capacity,
    Get_CanRead,
    Get_CanWrite,
    Read,
    Write
};
 
 
//初始化环形缓冲区
static int Create(_loopList_s* p,unsigned char *buf,unsigned int len)
{
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: input list is NULL\n");
#endif
        return 0;
    }
    p->capacity = len;
    p->buf = buf;
    p->head = p->buf;//头指向数组首地址
    p->tail = p->buf;//尾指向数组首地址
 
    return 1;
}
 
//删除一个环形缓冲区
static void Delete(_loopList_s* p)
{
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: input list is NULL\n");
#endif
        return;
    }
 
    p->buf  = NULL;//地址赋值为空
    p->head = NULL;//头地址为空
    p->tail = NULL;//尾地址尾空
    p->capacity = 0;//长度为空
}
 
//获取链表的长度
static int Get_Capacity(_loopList_s *p)
{
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: input list is NULL\n");
#endif
        return -1;
    }
    return p->capacity;
}
 
//返回能读的空间
static int Get_CanRead(_loopList_s *p)
{
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: input list is NULL\n");
#endif
        return -1;
    }
 
    if(p->head == p->tail)//头与尾相遇
    {
        return 0;
    }
 
    if(p->head < p->tail)//尾大于头
    {
        return p->tail - p->head;
    }
 
    return Get_Capacity(p) - (p->head - p->tail);//头大于尾
}
 
//返回能写入的空间
static int Get_CanWrite(_loopList_s *p)
{
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: input list is NULL\n");
#endif
        return -1;
    }
 
    return Get_Capacity(p) - Get_CanRead(p);//总的减去已经写入的空间
}
 
 
//  p--要读的环形链表
//  buf--读出的数据
//  count--读的个数
static int Read(_loopList_s *p, void *buf, unsigned int len)
{
    int copySz = 0;
 
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: input list is NULL\n");
#endif
        return -1;
    }
 
    if(NULL == buf)
    {
#if DEBUG_LOOP
        printf("ERROR: input buf is NULL\n");
#endif
        return -2;
    }
 
    if(p->head < p->tail)//尾大于头
    {
        copySz = min(len, Get_CanRead(p));	//比较能读的个数
        memcpy(buf, p->head, copySz);		//读出数据
        p->head += copySz;					//头指针加上读取的个数
        return copySz;						//返回读取的个数
    }
    else //头大于等于了尾
    {
        if (len < Get_Capacity(p)-(p->head - p->buf))//读的个数小于头上面的数据量
        {
            copySz = len;//读出的个数
            memcpy(buf, p->head, copySz);
            p->head += copySz;
            return copySz;
        }
        else//读的个数大于头上面的数据量
        {
            copySz = Get_Capacity(p) - (p->head - p->buf);//先读出来头上面的数据
            memcpy(buf, p->head, copySz);
            p->head = p->buf;//头指针指向数组的首地址
            //还要读的个数
            copySz += Read(p,(char*)buf+copySz, len-copySz);//接着读剩余要读的个数
            return copySz;
        }
    }
}
//  p--要写的环形链表
//  buf--写出的数据
//  len--写的个数
static int Write(_loopList_s *p, const void *buf, unsigned int len)
{
    int tailAvailSz = 0;//尾部剩余空间
 
    if(NULL == p)
    {
#if DEBUG_LOOP
        printf("ERROR: list is empty \n");
#endif
        return -1;
    }
 
    if(NULL == buf)
    {
#if DEBUG_LOOP
        printf("ERROR: buf is empty \n");
#endif
        return -2;
    }
 
    if (len >= Get_CanWrite(p))//如果剩余的空间不够
    {
#if DEBUG_LOOP
        printf("ERROR: no memory \n");
#endif
        return -3;
    }
 
    if (p->head <= p->tail)//头小于等于尾
    {
        tailAvailSz = Get_Capacity(p) - (p->tail - p->buf);	//查看尾上面剩余的空间
        if (len <= tailAvailSz)//个数小于等于尾上面剩余的空间
        {
            memcpy(p->tail, buf, len);//拷贝数据到环形数组
            p->tail += len;//尾指针加上数据个数
            if (p->tail == p->buf+Get_Capacity(p))//正好写到最后
            {
                p->tail = p->buf;//尾指向数组的首地址
            }
            return len;//返回写入的数据个数
        }
        else
        {
            memcpy(p->tail, buf, tailAvailSz);	//填入尾上面剩余的空间
            p->tail = p->buf;					//尾指针指向数组首地址
            //剩余空间                   剩余数据的首地址       剩余数据的个数
            return tailAvailSz + Write(p, (char*)buf+tailAvailSz, len-tailAvailSz);//接着写剩余的数据
        }
    }
    else //头大于尾
    {
        memcpy(p->tail, buf, len);
        p->tail += len;
        return len;
    }
}
 
 
/*********************************************END OF FILE********************************************/

头文件

#ifndef __FY_LOOPLIST_H
#define __FY_LOOPLIST_H
 
//环形缓冲区数据结构
typedef struct {
    unsigned int   capacity;	//空间大小
    unsigned char  *head; 	//头
    unsigned char  *tail; 	//尾
    unsigned char  *buf; 	//数组的首地址
} _loopList_s;
 
struct _typdef_LoopList
{
    int	(*Create)	(_loopList_s* p,unsigned char *buf,unsigned int len);
    void (*Delete)(_loopList_s* p);
    int (*Get_Capacity)(_loopList_s *p);
    int (*Get_CanRead)(_loopList_s *p);
    int (*Get_CanWrite)(_loopList_s *p);
    int (*Read)(_loopList_s *p, void *buf, unsigned int len);
    int (*Write)(_loopList_s *p, const void *buf, unsigned int len);
};
 
extern struct _typdef_LoopList _list;
 
#endif

1、整体思路

1、把64K的flash空间分成了4个部分,第一部分是BootLoader,第二部分是程序A(APP1),第三部分是程序B(APP2),第四部分是用来存储一些变量和标记的。下面是空间的分配情况。BootLoader程序可以用来更新程序A,而程序A又更新程序B,程序B可以更新程序A。最开始的时候想的是程序A、B都带更新了干嘛还多此一举,其实这个Bootloader还是需要的。如果之后程序A、B和FLAG三部分,假设一种情况,在程序B中更新程序A中遇到问题,复位后直接成砖,因为程序A在其实地址,上电直接运行程序A,而程序A现在出问题了,那就没招了。所以加上BootLoader情况下,不管怎么样BootLoader的程序是不会错的,因为更新不会更新BootLoader,计时更新出错了,还可以进入BootLoader重新更新应用程序。我见也有另外一种设计方法的,就是应用程序只有一个程序A,把程序B区域的flash当作缓存用,重启的时候判断B区域有没有更新程序,有的话就把B拷贝到A,然后擦除B,我感觉这样其实也一样,反正不管怎么样这部分空间是必须要预留出来的。

“基于STM32的串口环形队列IAP调试心得"

这里在keil中配置的只有起始地址和大小,并没有结束地址,我这里也就不详细计算了。总体就是这样的。

2、Bootloader部分

BootLoader的任务有两个,一是在串口中断接收BIN的数据和主循环内判断以及更新APP1的程序,二是在在程序开始的时候判断有没有可用的用户程序进而跳转到用户程序(程序A或者程序B)。

简单介绍下执行流程:

系统上电首先肯定是执行BootLoader程序的,因为它的起始地址就是0x08000000,首先是初始化,然后判断按键是否手动升级程序,按键按下了就把FLAG部分的APP标记写成0xFFFF(这里用的宏定义方式),再执行执行App_Check(),否则就直接执行App_Check()。

App_Check函数是来判断程序A和程序B的,最开始BootLoader是用swd方式下载的,下载的时候全片擦除,所以会执行主循环的Update_Check函数。此时串口打印出“等待接收APP1的BIN”,这个时候发送APP1的BIN过去,等接受完了,会写在FLAG区域写个0xAAAA,代表程序A写入了,下次启动可以执行程序A。

主要代码部分

#include "fy_includes.h"
 
/*
晶振使用的是16M 其他频率在system_stm32f10x.c中修改
使用printf需要在fy_includes.h修改串口重定向为#define PRINTF_USART USART1 
*/
 
 
/*
Bootloader程序
完成三个任务
步骤1.检查是否有程序更新,如果有就擦写flash进行更新,如果没有进入步骤2
步骤2.判断app1有没有可执行程序,如果有就执行,如果没有进入步骤3
步骤3.串口等待接收程序固件
*/
 
#define FLAG_UPDATE_APP1 0xBBAA
#define FLAG_UPDATE_APP2 0xAABB
#define FLAG_APP1 0xAAAA
#define FLAG_APP2 0xBBBB
#define FLAG_NONE 0xFFFF
 
_loopList_s list1;
u8 rxbuf[1024];
u8 temp8[2]; 
u16 temp16;
u32 rxlen=0;
u32 applen=0;
u32 write_addr;
u8 overflow=0;
u32 now_tick=0;
u8 _cnt_10ms=0;
 
static void App_Check(void)
{
	//获取程序标号
	STMFLASH_Read(FLASH_PARAM_ADDR,&temp16,1);
 
	if(temp16 == FLAG_APP1)//执行程序A
	{
		if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//可执行?
		{
			printf(" 执行程序A...\r\n");			
			IAP_RunApp(FLASH_APP1_ADDR);
		}
		else
		{
			printf(" 程序A不可执行,擦除APP1程序所在空间...\r\n");
			for(u8 i=10;i<35;i++)
			{
				STMFLASH_Erase(FLASH_BASE + i*STM_SECTOR_SIZE,512);
			}
			printf(" 程序A所在空间擦除完成... \r\n");
			printf(" 将执行程序B... \r\n");
			if(((*(vu32*)(FLASH_APP2_ADDR+4))&0xFF000000)==0x08000000)//可执行?
			{
				printf(" 执行程序B...\r\n");				
				IAP_RunApp(FLASH_APP2_ADDR);
			}
			else
			{
				printf(" 程序B不可执行,擦除APP2程序所在空间...\r\n");
				for(u8 i=35;i<60;i++)
				{
					STMFLASH_Erase(FLASH_BASE + i*STM_SECTOR_SIZE,512);
				}
				printf(" 程序B所在空间擦除完成...\r\n");
			}
		}
	}
	
	if(temp16 == FLAG_APP2)//执行程序B
	{
		if(((*(vu32*)(FLASH_APP2_ADDR+4))&0xFF000000)==0x08000000)//可执行?
		{
			printf(" 执行程序B...\r\n");			
			IAP_RunApp(FLASH_APP2_ADDR);
		}
		else
		{
			printf(" 程序B不可执行,擦除APP2程序所在空间... \r\n");
			for(u8 i=35;i<60;i++)
			{
				STMFLASH_Erase(FLASH_BASE + i*STM_SECTOR_SIZE,512);
			}
			printf(" 程序B所在空间擦除完成... \r\n");
			printf(" 将执行程序A... \r\n");
			if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//可执行?
			{
				printf(" 执行程序A...\r\n");				
				IAP_RunApp(FLASH_APP1_ADDR);
			}
			else
			{
				printf(" 程序A不可执行,擦除APP1程序所在空间...\r\n");
				for(u8 i=10;i<35;i++)
				{
					STMFLASH_Erase(FLASH_BASE + i*STM_SECTOR_SIZE,512);
				}
				printf(" 程序A所在空间擦除完成...\r\n");
			}
		}
	}
	
	if(temp16 == FLAG_NONE)
	{
		printf(" 擦除App1程序所在空间...\r\n");
		for(u8 i=10;i<35;i++)
		{
			STMFLASH_Erase(FLASH_BASE + i*STM_SECTOR_SIZE,512);
		}
		printf(" 程序A所在空间擦除完成...\r\n");
	}
}
 
 
static void Update_Check(void)
{
	if(_list.Get_CanRead(&list1)>1)
	{
		_list.Read(&list1,&temp8,2);//读取两个数据
		 
		temp16 = (u16)(temp8[1]<<8) | temp8[0];
					
		STMFLASH_Write(write_addr,&temp16,1);
		write_addr+=2;
	}
 
	if(GetSystick_ms() - now_tick >10)//10ms
	{
		now_tick = GetSystick_ms();
		_cnt_10ms++;
		if(applen == rxlen && rxlen)//接收完成
		{
			if(overflow)
			{
				printf("接收溢出,无法更新,请重试 \r\n");
				SoftReset();//软件复位
			}
			else
			{
				printf(" \r\n 接收BIN文件完成,长度为 %d \r\n",applen);
				
				temp16 = FLAG_APP1;
				STMFLASH_Write(FLASH_PARAM_ADDR,&temp16,1);//写入标记
				temp16 = (u16)(applen>>16);
				STMFLASH_Write(FLASH_PARAM_ADDR+2,&temp16,1);
				temp16 = (u16)(applen);
				STMFLASH_Write(FLASH_PARAM_ADDR+4,&temp16,1);
				
				SoftReset();//软件复位
			}
		}else applen = rxlen;//更新长度
	}
	if(_cnt_10ms>=50)
	{
		_cnt_10ms=0;
		Led_Tog();
		if(!rxlen)
		{
			printf(" 等待接收App1的BIN文件 \r\n");
		}
	}
}
int main(void)
{
	NVIC_SetPriorityGrouping( NVIC_PriorityGroup_2);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);	 //开启AFIO时钟
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);	//禁止JTAG保留SWD
    
	Systick_Configuration();
    Led_Configuration();
	Key_Configuration();
	Usart1_Configuration(9600);
	USART_ITConfig(USART1, USART_IT_IDLE, DISABLE);//关闭串口空闲中断
 
	printf(" this is bootloader!\r\n\r\n");
	if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) == SET)
	{
		Delay_ms(100);
		if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) == SET)//开机按下keyup进行更新
		{
			printf(" 主动更新,");
			temp16 = FLAG_NONE;
			STMFLASH_Write(FLASH_PARAM_ADDR,&temp16,1);
		}
		else
		{
			
		}
	}
 
	App_Check();
	
	printf(" 执行BootLoader程序... \r\n");
	_list.Create(&list1,rxbuf,sizeof(rxbuf));
 
	write_addr = FLASH_APP1_ADDR;
	
    while(1)
    {		
		Update_Check();
    }
}
 
//USART1串口中断函数
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
		u8 temp = USART1->DR;
		if(_list.Write(&list1,&temp,1)<=0)
		{
			overflow=1;
		}
		rxlen++;
    }
}

其中的宏:

//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000     //STM32 FLASH的起始地址
#define FLASH_APP1_ADDR    STM32_FLASH_BASE+0x2800    //偏移10K
#define FLASH_APP2_ADDR    STM32_FLASH_BASE+0x8c00    //偏移35K
#define FLASH_PARAM_ADDR    STM32_FLASH_BASE+0xF000    //偏移60K

3、程序A和程序B部分

这两个都是用户程序,这两个程序都带有更新程序功能,我这里用作测试的A和B程序大体都差不多,不同的地方就是程序A接收的BIN用来更新程序B,程序B接收的BIN用来更新A,还有就是中断向量表便宜不同以及打印输出不同。

应用程序部分没什么说的,程序A和B很类似,这里贴上A的代码

#include "fy_includes.h"
 
/*
晶振使用的是16M 其他频率在system_stm32f10x.c中修改
使用printf需要在fy_includes.h修改串口重定向为#define PRINTF_USART USART1 
*/
 
 
/*
APP1程序
完成两个任务
1.执行本身的app任务,同时监听程序更新,监听到停止本身的任务进入到状态2
2.等待接收完成,完成后复位重启
*/
 
#define FLAG_UPDATE_APP1 0xBBAA
#define FLAG_UPDATE_APP2 0xAABB
#define FLAG_APP1 0xAAAA
#define FLAG_APP2 0xBBBB
#define FLAG_NONE 0xFFFF
 
_loopList_s list1;
u8 rxbuf[1024];
u8 temp8[2]; 
u16 temp16;
u32 rxlen=0;
u32 applen=0;
u32 write_flsh_addr;
u8 update=0;
u8 overflow=0;
u32 now_tick;
u8 _cnt_10ms=0;
 
static void Update_Check(void)
{
	if(update)//监听到有更新程序
	{
		write_flsh_addr = FLASH_APP2_ADDR;//App1更新App2的程序
		overflow=0;
		rxlen=0;
		_list.Create(&list1,rxbuf,sizeof(rxbuf));
		
		printf(" 擦除APP2程序所在空间...\r\n");
		for(u8 i=35;i<60;i++)//擦除APP2所在空间程序
		{
			STMFLASH_Erase(FLASH_BASE + i*STM_SECTOR_SIZE,512);
		}
		printf(" 程序B所在空间擦除完成...\r\n");
		
		while(1)
		{
			if(_list.Get_CanRead(&list1)>1)
			{
				_list.Read(&list1,&temp8,2);//读取两个数据
				 
				temp16 = (u16)(temp8[1]<<8) | temp8[0];
							
				STMFLASH_Write(write_flsh_addr,&temp16,1);
				write_flsh_addr+=2;
			}
			
			if(GetSystick_ms() - now_tick >10)//10ms
			{
				now_tick = GetSystick_ms();
				_cnt_10ms++;
				if(applen == rxlen && rxlen)//接收完成
				{
					if(overflow)
					{
						printf(" \r\n 接收溢出,请重新尝试 \r\n");
						SoftReset();//软件复位
					}
					
					printf(" \r\n 接收BIN文件完成,长度为 %d \r\n",applen);
					
					temp16 = FLAG_APP2;
					STMFLASH_Write(FLASH_PARAM_ADDR,&temp16,1);//写入标记
					temp16 = (u16)(applen>>16);
					STMFLASH_Write(FLASH_PARAM_ADDR+2,&temp16,1);
					temp16 = (u16)(applen);
					STMFLASH_Write(FLASH_PARAM_ADDR+4,&temp16,1);
					
					printf(" 系统将重启....\r\n");
					SoftReset();//软件复位
				}else applen = rxlen;//更新长度				
			}
			
			if(_cnt_10ms>=50)
			{
				_cnt_10ms=0;
				Led_Tog();
				if(!rxlen)
				{
					printf(" 等待接收App2的BIN文件 \r\n");
				}
			}
		}//while(1)
	}
}
 
 
static void App_Task(void)
{
	if(GetSystick_ms() - now_tick >500)					
	{
		now_tick = GetSystick_ms();
		printf(" 正在运行APP1 \r\n");
		Led_Tog();
	}
}
 
int main(void)
{
	SCB->VTOR = FLASH_APP1_ADDR; 	 
 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);	 //开启AFIO时钟
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);	//禁止JTAG保留SWD
    
	Systick_Configuration();
    Led_Configuration();
	Usart1_Configuration(9600);
	printf(" this is APP1!\r\n");
	
	Delay_ms(500);
	
    while(1)
    {
		Update_Check();
		App_Task();
    }
}
 
//USART1串口中断函数
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
		u8 temp = USART1->DR;
		if(update)
		{			
			if(_list.Write(&list1,&temp,1) <= 0 )
			{
				overflow = 1;
			}
		}
		else
		{
			rxbuf[rxlen] = temp;
		}
		rxlen++;
    }
    if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
    {
		u8 temp = USART1->DR;
		temp = USART1->SR;
		
		if(strstr((char *)rxbuf,"App Update") && rxlen)
		{
			update=1;
			USART_ITConfig(USART1, USART_IT_IDLE, DISABLE);//关闭串口空闲中断
		}
		else
		{
			Usart1_SendBuf(rxbuf,rxlen);
		}
		rxlen=0;
    }
	
}

这里如果要移植需要注意的就是向量表的偏移以及更新擦写的区域。

4、剩余的4Kflash空间部分

这里其实只是用来存储2个变量,一个是程序运行标记,一个是接收到的程序长度,程序标记还有点把子用,程序长度其实要不要都无所谓。

5、遇到的坑

最值得一说的就是更新部分,最开始程序没有加入擦除flash,遇到的情况就是下载完BootLoader后发送app1没问题,在app1中更新App2也没问题,然后app2再更新app1就出问题了。直观的结果就是循环队列溢出,原因就是app2在更新app1前没有去擦除app1所在的flash,所以在写的时候就要去擦除,这样就写的很慢,然而串口接收是不停的收,所以就是写不过来。

版权声明:本文为CSDN博主「沉默的小宇宙」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/qq997758497/article/details/90211868
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 127

引言

X-CUBE-CRYPTOLIB 是基于 STM32 的 Crypto 算法库,支持对称密钥、非对称密钥、哈希等多种算法。正确地使用 Cyrptolib 算法库,可以在应用程序中实现数据加密、设备身份认证、加密通信等多种应用层所需的安全功能。相反,若不能正确地使用算法库往往会带来加解密数据错误等系列问题。

关于 STM32 Crypto 算法库应用中的常见的问题之一就是应用程序没有使能 MCU 的CRC 模块,尽管输出的数据和期望值不同,但加解密函数的调用并未返回异常。本文在此描述另外一种没有正确使用算法库的情况。

问题描述

客户应用项目中需要在固件更新过程中对固件进行加密并验证,根据推荐采用了 AES- GCM 算法完成该任务。下载的固件通过 AES-GCM 进行加密,并带 TAG 可以用于验证固件来源的合法性。在项目的代码中使用 X-Cube-Cryptolib 进行 AES-GCM 运算。上位机使用 cryptopp820 加密库对固件目标 bin 文件进行加密,然后在 MCU 上通过X-Cube- Cryptolib 加密库进行 AES-GCM128 解密,解密数据没有问题,但是 TAG 数据总是无法校验通过。

问题分析与定位

正常情况下,当调用 AES_GCM_Decrypt_Finish(P_pAESGCMctx, NULL, P_pOutputSize);执行后,AESctx.mFlags 结果会提示是否通过校验。如果校验成功,该值应该等于 0x22,而运行结果中看到的却是 0x12。

确认库函数使用方法

将调用 AES-GCM 功能的代码放在 X-Cube-Cryptolib 中一个简单的测试程序的环境进行测试,查看是否有该问题,结果发现测试程序中 AES-GCM 校验是可以成功的,但是集成到客户应用中时就无法成功。那我们接下来重点研究应用程序环境。

应用程序环境

应用程序使用了 FreeRTOS,基于 IAR 编译环境。

查看库文件的使用

确认使用了正确的库文件。

确认是否存在多线程访问

AES-GCM 的函数会在几个线程中调用,而且确认不会出现同时调用的情况,不存在 raise condition 的问题。

查看内存使用情况

最初怀疑是否因为任务栈溢出造成,于是查看内存使用情况。

  • IAR stack size: 0x4800
  • IAR heap size: 0x4000
  • FreeRTOS heap size: 85KB
  • 执行AES 运算的线程 stack size: 2560B

通过 FreeRTOS 的 uxTaskGetStackHighWaterMark() 函数查看该线程还有 500 字节左右剩余空间。

AESGCMctx_stt 结构的大小有 2360 字节,AES-GCM 加解密函数需要的 stack 大小大概在 450 字节左右,但是应用代码中将该变量定义为全局变量,以便可以在几个不同的线程中使用,这样可以确认线程栈大小没有问题,不存在 stack overflow 的问题。

查看生成代码的.map 文件

通过比较,所有 cryptolib 中的 symbol 在 map 中的大小都正常,唯一有问题的是 AESGCMctx_stt 结构变量的大小。

  • 应用代码.map:AESctx 0x2002f1f4 0x8f8 Data Gb AES_GCM_Decrypt.o [1]
  • 正常测试代码 .map:AESctx 0x20002e64 0x938 Data Gb AES_GCM.o [1]

验证不过的问题应该和这个结构的数据有直接关系,接下来研究是什么造成了这个不同。

查看项目使用的 crypto 库头文件

经检查,INCLUDE_AES192 和 INCLUDE_AES256 两个宏定义在 config.h 的定义中被注释掉,这将导致 aes_gcm.h 中 AESGCMctx_stt 数据结构的成员变量 uint32_t amExpKey[CRL_AES_MAX_EXPKEY_SIZE];的大小发生变化,因为CRL_AES_MAX_EXPKEY_SIZE 的定义根据INCLUDE_AES128/192/256 是否定义会有所不同。

//#define INCLUDE_DES ((uint16_t)0x0001) /*!< DES functions are included in the library. */
//#define INCLUDE_TDES ((uint16_t)0x0002) /*!< TripleDES (TDES) functions are included in the library. */
#define INCLUDE_AES128 ((uint16_t)0x0004) /*!< AES functions with key size of 128 bit are included in the library. */
//#define INCLUDE_AES192 ((uint16_t)0x0008) /*!< AES functions with key size of 192 bit are included in the library. */
//#define INCLUDE_AES256 ((uint16_t)0x0010) /*!< AES functions with key size of 256 bit are included in the library. */
//#define INCLUDE_ARC4 ((uint16_t)0x0020) /*!< ARC4 functions are included in the library. */
//#define INCLUDE_CHACHA ((uint16_t)0x0040) /*!< ChaCha functions are included in the library. */
//#define INCLUDE_CHACHA20POLY1305 ((uint16_t)0x0080) /*!< oly1305- AES functions are included in the library */

问题解决

将 config.h 里面被注释掉的两行定义打开,重新编译,此时 TAG 验证能够正常通过。

#define INCLUDE_AES128 ((uint16_t)0x0004) /*!< AES functions with key size of 128 bit are included in the library. */
#define INCLUDE_AES192 ((uint16_t)0x0008) /*!< AES functions with key size of 192 bit are included in the library. */
#define INCLUDE_AES256 ((uint16_t)0x0010) /*!< AES functions with key size of 256 bit are included in the library. */

小结

简言之,如果使用 X-Cube-Cryptolib 库的话,作为用户就不要改动 config.h 的内容, 不可想当然地自行调整配置。其实,库是预先编译好了的,所有的功能都已经包含,只是链接的时候根据用户使用到的函数去链接最终的目标文件。这个客户就是按照以为关闭某些配置可以节省代码空间的想法,贸然注释掉了他以为自己不需要的功能,造成数据结构大小发生变化等,影响加密库的正常使用。

来源:STM32单片机
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 236

什么是 STM32

STM32,从字面上来理解,ST是意法半导体,M是Microelectronics的缩写,32表示32位,合起来理解,STM32就是指ST公司开发的32位微控制器。在如今的32位控制器当中,STM32可以说是最璀璨的新星,它受宠若娇,大受工程师和市场的青睐,无芯能出其右。

STM32属于一个微控制器,自带了各种常用通信接口,比如USART、I2C、SPI等,可接非常多的传感器,可以控制很多的设备。现实生活中,我们接触到的很多电器产品都有STM32的身影,比如智能手环,微型四轴飞行器,平衡车、移动POST机,智能电饭锅,3D打印机等等。下面我们以最近最为火爆的两个产品来讲解下,一个是手环,一个是飞行器。

现在无人机非常火热,高端的无人机用STM32做不来,但是小型的四轴飞行器用STM32还是绰绰有余的。

STM32 分类

STM32有很多系列,可以满足市场的各种需求,从内核上分有Cortex-M0、M3、M4和M7这几种,每个内核又大概分为主流、高性能和低功耗。具体如下表所示。

“STM32要怎么选型?"

单纯从学习的角度出发,可以选择F1和F4,F1代表了基础型,基于Cortex-M3内核,主频为72MHZ,F4代表了高性能,基于Cortex-M4内核,主频180M。之于F1,F4(429系列以上)除了内核不同和主频的提升外,升级的明显特色就是带了LCD控制器和摄像头接口,支持SDRAM,这个区别在项目选型上会被优先考虑。但是从大学教学和用户初学来说,还是首选F1系列,目前在市场上资料最多,产品占有量最多的就是F1系列的STM32。

以STM32F103VET6来讲下STM32的命名方法,具体如下表所示。

“STM32要怎么选型?"

更详细的命名方法说明,见下图。

“STM32要怎么选型?"

选择合适的 MCU

了解了STM32的分类和命名方法之后,就可以根据项目的具体需求先大概选择哪类内核的MCU,普通应用,不需要接大屏幕的一般选择Cortex-M3内核的F1系列,如果要追求高性能,需要大量的数据运算,且需要外接RGB大屏幕的则选择Cortex-M4内核的F429系列。明确了大方向之后,接下来就是细分选型,先确定引脚,引脚多的功能就多,价格也贵,具体得根据实际项目中需要使用到什么功能,够用就好。确定好了引脚数目之后再选择FLASH大小,相同引脚数的MCU会有不同的FLASH大小可供选择,这个也是根据实际需要选择,程序大的就选择大点的FLASH,要是产品一量产,这些省下来的都是钱啊。有些月出货量以KK(百万数量级)为单位的产品,不仅是MCU,连电阻电容能少用就少用,更甚者连PCB的过孔的多少都有讲究。项目中的元器件的选型有很多学问。

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 392

本文我们就来学习 STM32F1 的DMA 使用。要实现的功能是:通过 K_UP 按键控制 DMA 串口 1 数据的传送,在传送过程中让 D2 指示灯不断闪烁,直到数据传送完成。D1 指示灯闪烁提示系统正常运行。学习时可以参考《STM32F10x 中文参考手册》-10 DMA 控制器(DMA)章节。

DMA 简介

DMA,全称是 Direct Memory Access,中文意思为直接存储器访问。DMA 可用于实现外设与存储器之间或者存储器与存储器之间数据传输的高效性。之所以称为高效, 是因为 DMA 传输数据移动过程无需 CPU 直接操作, 这样节省的 CPU 资源就可供其它操作使用。从硬件层面来理解,DMA 就好像是 RAM 与 I/O 设备间数据传输的通路, 外设与存储器之间或者存储器与存储器之间可以直接在这条通路上进行数据传输。这里说的外设一般指外设的数据寄存器, 比如 ADC、SPI、I2C、DCMI等外设的数据寄存器, 存储器一般是指片内 SRAM、 外部存储器、 片内 Flash等。

STM32F1 最多有 2 个 DMA 控制器 ( DMA2 仅存在大容量产品中) ,DMA1 有7 个通道。DMA2 有 5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个 DMA 请求的优先权。

STM32F1 的 DMA 有以下主要特性:

● 12 个独立的可配置的通道(请求):DMA1 有 7 个通道, DMA2 有 5 个通道

● 每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能通过软件来配置。

● 在同一个 DMA 模块上, 多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求 0 优先于请求1,依此类推) 。

● 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。

● 支持循环的缓冲器管理

● 每个通道都有 3 个事件标志(DMA 半传输、 DMA 传输完成和 DMA 传输出错),这3 个事件标志逻辑或成为一个单独的中断请求。

● 存储器和存储器间的传输

● 外设和存储器、存储器和外设之间的传输

● 闪存、 SRAM、外设的 SRAM、 APB1、 APB2 和 AHB 外设均可作为访问的源和目标。

● 可编程的数据传输数目:最大为 65535

DMA 结构框图

DMA 控制器独立于内核,属于一个单独的外设,结构比较简单,从编程的角度来看,我们只需掌握结构框图中的三部分内容即可。如图所示(大家也可以查看《STM32F10x中文参考手册》-10 DMA 控制器(DMA)章节内容)。

“STM32实例-DMA

我们把 DMA 结构框图分成3个子模块,按照顺序依次进行简单介绍。

(1)标号 1:DMA 请求

如果外设要想通过 DMA 来传输数据, 必须先给 DMA 控制器发送 DMA 请求,DMA 收到请求信号之后, 控制器会给外设一个应答信号, 当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。

根据前面介绍我们知道,DMA 含有 DMA1 和 DMA2 两个控制器,其中 DMA1 含有 7个通道,DMA2 含有 5 个通道,不同的 DMA 控制器的通道对应着不同的外设请求。

从DMA 请求映射图中可以知道各通道所对应的外设请求,如图分别是DMA1和DMA2请求映射图:

“STM32实例-DMA

“STM32实例-DMA

DMA2 请求通道中 ADC3、 SDIO 和 TIM8 的 DMA 请求只在大容量产品中存在,这个在具体项目时要注意。

(2)标号 2:DAM 通道

DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道, DMA2 有 5个通道,每个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。

(3)标号 3:仲裁器

当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在 DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 0高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。

DMA 数据配置

使用 DMA, 最核心就是配置要传输的数据, 包括数据从哪里来, 要到哪里去,传输的数据的单位是什么,要传多少数据,是一次传输还是循环传输等。

(1)从哪里来到哪里去

我们知道 DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。具体的方向 DMA_CCR 位 4 DIR 配置:0 表示从外设到存储器, 1 表示从存储器到外设。这里面涉及到的外设地址由 DMA_CPAR 配置,存储器地址由 DMA_CMAR配置。

外设到存储器

当我们使用从外设到存储器传输时,以 ADC 采集为例。DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。

存储器到外设

当我们使用从存储器到外设传输时, 以串口向电脑端发送数据为例。DMA 外设寄存器的地址对应的就是串口数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。

存储器到存储器

当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH当作一个外设来看)的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。跟上面两个不一样的是,这里需要把 DMA_CCR 位14:MEM2MEM:存储器到存储器模式配置为 1,启动 M2M 模式。

(2)要传多少,单位是什么

当我们配置好数据要从哪里来到哪里去之后, 我们还需要知道我们要传输的数据是多少,数据的单位是什么。

以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由 DMA_CNDTR 配置,这是一个 32 位的寄存器,一次最多只能传输 65535 个数据。

要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8 位的,所以我们定义的要发送的数据也必须是 8 位。外设的数据宽度由 DMA_CCR 的 PSIZE[1:0]配置,可以是 8/16/32 位,存储器的数据宽度由DMA_CCR 的 MSIZE[1:0]配置,可以是 8/16/32 位。

在 DMA 控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。外设的地址指针由 DMA_CCRx 的PINC 配置,存储器的地址指针由 MINC 配置。以串口向电脑发送数据为例,要发送的数据很多,每发送完一个,那么存储器的地址指针就应该加 1,而串口数据寄存器只有一个,那么外设的地址指针就固定不变。具体的数据指针的增量模式由实际情况决定。

(3)什么时候传输完成

数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来判断。每个 DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。有关各个标志位的详细描述请参考 DMA 中断状态寄存器DMA_ISR 的详细描述。

传输完成还分两种模式,是一次传输还是循环传输,一次传输很好理解,即传输一次之后就停止,要想再传输的话,必须关断 DMA 使能后再重新配置后才能继续传输。循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。具体的由 DMA_CCR 寄存器的 CIRC 循环模式位控制。

DMA 配置步骤

接下来我们介绍下如何使用库函数对 DMA 进行配置。这个也是在编写程序中必须要了解的。具体步骤如下:(DMA 相关库函数在 stm32f10x_dma.c 和stm32f10x_dma.h 文件中)

(1)使能 DMA 控制器(DMA1 或 DMA2)时钟

要使能 DMA 时钟,需通过AHB1ENR 寄存器来控制,使能 DMA时钟库函数为:

void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState
NewState);

例如使能 DMA1 时钟,函数如下:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

(2)初始化 DMA 通道,包括配置通道、外设和内存地址、传输数据量等要使用 DMA,必须对其相关参数进行设置,包括通道选择、外设和内存地址、

通道优先级、传输数据量的配置等。该部分设置通过 DMA 初始化函数 DMA_Init完成的:

void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx,DMA_InitTypeDef*
DMA_InitStruct);

函数中第一个参数是用来确定 DMA 通道,参数范围为:

DMA1_Channel_0~DMA1_Channel_7(DMA2 是DMA2_Channel_0-DMA2_Channel_5)

第二个参数是一个结构体指针变量,结构体类型是 DMA_InitTypeDef,其内包含了 DMA 相关参数的设置。下面我们简单介绍下它的成员:

typedef struct
{
uint32_t DMA_PeripheralBaseAddr; // 外设地址
uint32_t DMA_MemoryBaseAddr; // 存储器地址
uint32_t DMA_DIR; // 传输方向
uint32_t DMA_BufferSize; // 传输数目
uint32_t DMA_PeripheralInc; // 外设地址增量模式
uint32_t DMA_MemoryInc; // 存储器地址增量模式
uint32_t DMA_PeripheralDataSize; // 外设数据宽度
uint32_t DMA_MemoryDataSize; // 存储器数据宽度
uint32_t DMA_Mode; // 模式选择
uint32_t DMA_Priority; // 通道优先级
uint32_t DMA_M2M; // 存储器到存储器模式
} DMA_InitTypeDef;

DMA_PeripheralBaseAddr:外设地址,通过 DMA_CPAR 寄存器设置,一般设置为外设的数据寄存器地址,比如要进行串口 DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1->DR 的地址,表示方法为&USART1->DR。如果是存储器到存储器模式则设置为其中一个存储区地址。

DMA_Memory0BaseAddr:存储器地址,通过 DMA_CMAR 寄存器设置,一般设置为我们自定义存储区的首地址,即我们存放 DMA 传输数据的内存地址。比如我们定义一个 u32 类型数组,将数组首地址(直接使用数组名即可)赋值给DMA_Memory0BaseAddr,在DMA传输的时候就可以把数组内的数据发送或接收。

DMA_DIR:数据传输方向选择,可选择外设到存储器、存储器到外设以及存

储器到存储器。通过设定 DMA_CCR 寄存器的 DIR[1:0]位的值决定。比如本章实验是从内存读取数据发送到串口,所以数据传输方向为存储器到外设,配置为DMA_DIR_MemoryToPeripheral。

DMA_BufferSize:用来设置一次传输数据的大小,通过 DMA_CNDTR 寄存器设置。

DMA_PeripheralInc:用来设置外设地址是递增还是不变,通过 DMA_CCR寄存器的 PINC 位设置,如果设置为递增,那么下一次传输的时候地址加 1。通常外 设 只 有 一 个 数 据 寄 存 器 , 所 以 一 般 不 会 使 能 该 位 , 即 配 置 为DMA_PeripheralInc_Disable。

DMA_MemoryInc:用来设置内存地址是否递增,通过 DMA_CCR 寄存器的MINC位设置。我们自定义的存储区一般都是存放多个数据的,所以需要使能存储器地址自动递增功能,即配置为 DMA_MemoryInc_Enable。

DMA_PeripheralDataSize:外设数据宽度选择,可以为字节(8 位)、半字

(16位)、字(32 位),通过 DMA_CCR 寄存器的 PSIZE[1:0]位设置。例如本实验数据是按照 8 位字节传输,所以配置为DMA_PeripheralDataSize_Byte。

DMA_MemoryDataSize:存储器数据宽度选择,可以为字节(8 位)、半字(16位)、字(32 位),通过 DMA_CCR 寄存器的 MSIZE[1:0]位设置。本章实验同样设置为 8 位字节传输,这个要和我们定义的数组对应,所以配置为DMA_MemoryDataSize_Byte。

DMA_Mode:DMA 传输模式选择, 可选择一次传输或者循环传输, 通过DMA_CCR寄存器的 CIRC 位来设定。比如我们要从内存 (存储器) 中传输 64 个字节到串口,如果设置为循环传输,那么它会在 64 个字节传输完成之后继续从内存的第一个地址传输,如此循环。这里我们设置为一次传输完成之后不循环。所以设置值为DMA_Mode_Normal。

DMA_Priority:用来设置 DMA 通道的优先级,有低,中,高,超高四种级别,可通过 DMA_CCR 寄存器的PL[1:0]位来设定。DMA 优先级只有在多个 DMA 通道同时使用时才有意义,本章实验我们只使用了一个 DMA 通道,所以可以任意设置DMA 优先级,这里我们就设置为中等优先级,配置参数为DMA_Priority_Medium。

DMA_Priority:用来设置 DMA 通道的优先级,有低,中,高,超高四种级别,可通过 DMA_CCR 寄存器的PL[1:0]位来设定。DMA 优先级只有在多个 DMA 通道同时使用时才有意义,本章实验我们只使用了一个 DMA 通道,所以可以任意设置DMA 优先级,这里我们就设置为中等优先级,配置参数为DMA_Priority_Medium。

DMA_M2M:用来设置存储器到存储器模式,使用存储器到存储器时用到,设定 DMA_CCR 的位 14 MEN2MEN 即可启动存储器到存储器模式。

了解结构体成员功能后,就可以进行配置,本实验配置代码如下:

DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = mar;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//存储器到外
设模式
DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量
DMA_InitStructure.DMA_PeripheralInc =
DMA_PeripheralInc_Disable;//外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器
增量模式
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_Byte;//外设数据长度:8 位
DMA_InitStructure.DMA_MemoryDataSize =
DMA_MemoryDataSize_Byte;//存储器数据长度:8 位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先
级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA 通道 x 没有

设置为内存到内存传输

DMA_Init(DMAy_Channelx, &DMA_InitStructure);//初始化DMA

(3)使能外设 DMA功能(DMA 请求映射图对应的外设)

配置好 DMA 后,我们就需要使能外设 DMA 功能,例如我们要使能串口的DMA发送功能,调用的库函数为:

USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1 的 DMA发送

如果是要使能串口DMA接受, 那么第二个参数修改为USART_DMAReq_Rx即可。

如果是其他的外设需开启DMA功能, 只需要在对应的标准外设库函数中查找到对应的外设 DMA 使能函数。

(4)开启 DMA 的通道传输

初始化 DMA 后,要使用DMA还必须开启它,开启 DMA 通道传输的库函数为:

void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalStateNewState);

第一个参数为外设所对应的 DMA 通道,例如本章使用的是 USART1_TXDMA请求,因此它对应的是 DMA1_Channel4,可通过前面DMA 请求映射图选择。第二个参数相信不说也知道,就是使能或失能。

本实验使能 DMA1_Channel4函数为:

DMA_Cmd(DMA1_Channel4,ENABLE);

(5)查询 DMA 传输状态

通过以上 4 步设置,我们就可以启动一次 DMA 传输了。但是在 DMA 传输过程中,我们还需要查询 DMA传输通道的状态,使用的库函数是:

FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);

例如我们要查询 DMA1通道4 传输是否完成,方法是:

DMA_GetFlagStatus(DMA1_FLAG_TC4);

标准库中,还提供了获取当前剩余数据量大小的函数:

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef*DMAy_Channelx);

例如我们要获取 DMA1通道4 还有多少个数据没有传输,方法是:

DMA_GetCurrDataCounter(DMA1_Channel4);

同样,标准库中还提供了设置传输数据量大小的函数:

void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx,uint16_t DataNumber);

将以上几步全部配置好后,我们就可以使用 DMA 来传输对应外设的数据了。

硬件设计

本实验使用到硬件资源如下:

(1)D1 和 D2 指示灯

(2)K_UP 按键

(3)串口 1

(4)DMA

D1和 D2 指示灯、K_UP 按键、串口 1 电路在前面章节都介绍过,这里就不多说,至于 DMA 它属于 STM32F1 芯片内部的资源,只要通过软件配置好 DMA即可使用。D1指示灯用来提示系统运行状态,K_UP 按键用来控制 DMA发送,每按一次K_UP键,DMA 就将内存(自定义的一个数组)内数据发送 USART1,并通过串口 1将发送的内容打印出来,在 DMA 数据传输的过程中让 D2 指示灯不断闪烁,直到数据传输完成。D2 指示灯闪烁表示 CPU在执行其他的任务,说明 DMA 传输是不需要占用 CPU 的。

所要实现的功能是:通过 K_UP 按键控制 DMA 串口 1 数据的传送,在传送过程中让 D2 指示灯不断闪烁,直到数据传送完成。D1 指示灯闪烁提示系统正常运行。程序框架如下:

(1)初始化 USART1_TX对应的 DMA 通道相关参数

(2)编写主函数

前面介绍 DMA 配置步骤时, 就已经讲解如何初始化 DMA。下面我们打开 “DMA实验” 工程, 在 APP 工程组中可以看到添加了dma.c文件(里面包含了 DMA 驱动程序),在 StdPeriph_Driver 工程组中添加了stm32f10x_dma.c 库文件。DMA 操作的库函数都放在 stm32f10x_dma.c 和stm32f10x_dma.h 文件中,所以使用到 DMA 就必须加入 stm32f10x_dma.c文件,同时还要包含对应的头文件路径。

这里我们分析几个重要函数,其他部分程序大家可以打开工程查看。

DMA 初始化函数

要使用 DMA,我们必须先对它进行配置。初始化代码如下:

/****************************************************************
* 函 数 名 : DMAx_Init
* 函数功能 : DMA 初始化函数
* 输 入 :
DMAy_Channelx:DMA 通 道 选 择 ,@ref DMA_channel
DMA_Channel_0~DMA_Channel_7
par:外设地址
mar:存储器地址
ndtr:数据传输量
* 输 出 : 无
*****************************************************************/
void DMAx_Init(DMA_Channel_TypeDef* DMAy_Channelx,u32 par,u32
mar,u16 ndtr)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA1时钟使能
//DMA_DeInit(DMAy_Channelx);
/* 配置 DMA */
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = mar;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//存储器到外设模式
DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量
DMA_InitStructure.DMA_PeripheralInc =
DMA_PeripheralInc_Disable;//外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_Byte;//外设数据长度:8 位
DMA_InitStructure.DMA_MemoryDataSize =DMA_MemoryDataSize_Byte;//存储器数据长度:8 位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA 通道 x 没有设置为内存到内存传输
DMA_Init(DMAy_Channelx, &DMA_InitStructure);//初始化DMA
}

在 DMAx_Init()函数中,首先使能 DMA1 时钟,然后初始化 DMA 各参数,即配置 DMA_InitStructure结构体,初始化 DMA 通道,这一过程在前面步骤介绍中已经提了。

通道的选择有函数参数DMAy_Channelx传递进来, 函数中还有另外3个形参,par 传递的是外设地址,mar传递的是存储器(内存)地址,ndtr传递的是DMA数据传输量。这样做的好处是方便大家修改。

开启 DMA 传输函数

配置好 DMA 后,我们需要开启它,代码如下:

/****************************************************************
* 函 数 名 : DMAx_Enable
* 函数功能 : 开启一次 DMA 传输
* 输 入 : DMAy_Channelx:DMA 通道选择,@ref DMA_channel
DMA_Channel_0~DMA_Channel_7
ndtr:数据传输量
* 输 出 : 无
*****************************************************************/
void DMAx_Enable(DMA_Channel_TypeDef *DMAy_Channelx,u16 ndtr)
{
DMA_Cmd(DMAy_Channelx, DISABLE); //关闭
DMA 传输
DMA_SetCurrDataCounter(DMAy_Channelx,ndtr); //数据传输量
DMA_Cmd(DMAy_Channelx, ENABLE); //开启DMA传输
}

此函数功能很简单,首先失能 DMA 传输通道,然后设置传输的数据量大小,最后使能 DMA 传输通道。函数带有形参 DMAy_Channelx 和 ndtr,用于方便选择对应外设的通道和数据量。

主函数

编写好 DMA 的初始化和使能函数后, 接下来就可以编写主函数了, 代码如下:

/****************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*****************************************************************/
int main()
{
u8 i=0;
u8 key;
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2 组
LED_Init();
USART1_Init(9600);
KEY_Init();
DMAx_Init(DMA1_Channel4,(u32)&USART1->DR,(u32)send_buf,send_buf_len);
Send_Data(send_buf);
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP)
{
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1的DMA 发送
DMAx_Enable(DMA1_Channel4,send_buf_len); //开始一次DMA 传输!
//等待 DMA传输完成,此时我们来做另外一些事
//实际应用中,传输数据期间,可以执行另外的任务
while(1)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=0)//判断通道 4传输完成
{
DMA_ClearFlag(DMA1_FLAG_TC4);
break;
}
led2=!led2;
delay_ms(300);
}
}
i++;
if(i%20==0)
{
led1=!led1;
}
delay_ms(10);
}
}

主函数实现的功能很简单,首先调用之前编写好的硬件初始化函数,包括SysTick系统时钟, 中断分组, LED初始化等。然后调用我们前面编写的DMAx_Init函数,由于 USART1_TX 是在 DMA1 的通道 4 中,所以通道选择为 DMA1_Channel4。

外设地址传递的是&USART1->DR,因为 USART1 用来接收数据的寄存器是USART1->DR,加上一个&就转换成地址了。存储器地址为send_buf(数组名就是数组的地址) , 这个是我们自定义的一个u8类型数组, 数组大小为send_buf_len,这个也是我们定义的一个宏,值为 5000。然后调用Send_Data 函数,此函数功能是将数组 send_buf 内所有成员全部赋值为字符 5,代码如下:

#define send_buf_len 5000
u8 send_buf[send_buf_len];
void Send_Data(u8 *p)
{
u16 i;
for(i=0;i<send_buf_len;i++)
{
*p='5';
p++;
}
}

最后进入 while 循环,调用KEY_Scan 函数,不断检测 K_UP按键是否按下,如果 K_UP 按键按下,启动一次 USART_TX 的 DMA 传输,在传输的过程中让 CPU控制 D2指示灯闪烁,直到 DMA 数据传输完成。D1 指示灯间隔200ms 闪烁,提示系统正常运行。

将工程程序编译后下载到开发板内,可以看到 D1 指示灯不断闪烁,表示程序正常运行。当 K_UP 按键按下,DMA 开始将内存数组内的数据传输到串口1 上,同时传输过程中,D2 指示灯闪烁,直到传输完成。如果想在串口调试助手上看到传输信息,可以打开“串口调试助手”,首先勾选下标号 1 DTR 框,然后再取消勾选。这是因为此串口助手启动时会把系统复位住,通过 DTR 状态切换下即可。然后设置好波特率等参数后,串口助手上即会收到串口发送过来的信息。(串口助手上先勾选下标号1 DTR 框,然后再取消勾选)如图所示:

“STM32实例-DMA

来源:STM32嵌入式开发
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 166

页面

订阅 RSS - STM32