单片机

来源:strongerHuang(作者 | strongerHuang)

你可能听见过xxx单片机性能不行,或者太弱之类的话,那你了解这个性能是什么吗?

概述

我们经常听见某手机、电脑发布会,xxx跑分多少多少,其实这个跑分就是体现手机性能的一个指标。

我们使用STM32开发产品,选择MCU时,一般会综合考虑MCU各方面性能。

对于MCU来说,一个重要指标是功耗,还有一个重要指标就是性能。

当我们进入ST官方微控制器网页,就会发现ST将MCU划分为不同层次,如下图:

https://www.st.com/en/microcontrollers.html

1.png

从上面层次关系,大致可以了解ST 各MCU的功耗和性能。

STM32 CoreMark跑分

STM32家族的32位微控制器基于ARM Cortex®-M处理器设计,为用户提供新的自由度。

产品集高性能、实时性、数字信号处理、低功耗、低压运行、互联性于一体,保持充分的集成化和易用性。

根据性能划分,STM32各系列MCU划分为如下图几大类产品。具体可以参看官网信息。

https://www.st.com/en/microcontrollers/stm32-32-bit-arm-cortex-mcus.html

2.png

在嵌入式处理器领域最为知名和常见测试CPU性能的就是:Dhrystone 和 CoreMark。

1.CoreMark是用来衡量嵌入式系统中中心处理单元(CPU,或叫做微控制器MCU)性能的标准。

2.DMIPS:Dhrystone Million Instructions executed Per Second。用来计算同一秒内系统的处理能力,它的单位以百万来计算,也就是(MIPS)。主要用于测整数计算能力。

CoreMark跑分如何得来?

CoreMark是用来衡量CPU性能的标准。该标准于2009年由EEMBC组织的Shay Gla-On提出,并且试图将其发展成为工业标准,从而代替陈旧的Dhrystone标准。

与Dhrystone一样,CoreMark小巧,便携,易于理解,免费,并且显示单个数字基准分数。与Dhrystone不同,CoreMark具有特定的运行和报告规则,旨在避免Dhrystone的问题。

CoreMark跑分是通过运行C语言代码得出来的分数。主要包含如下的运算法则:列举(寻找并排序),数学矩阵操作(普通矩阵运算)和状态机(用来确定输入流中是否包含有效数字),最后还包括CRC(循环冗余校验)。

也就是说CoreMark是使用一套用C语言编辑的测试代码,我们通过运行这套代码就能测试你MCU的性能。

CoreMark测试代码

这里我们可以参考CoreMark官网信息,地址:

https://www.eembc.org/coremark/index.php


3.png


这里包含CoreMark相关说明、源代码下载、移植说明等。源代码位于Github,地址:
https://github.com/eembc/coremark


4.png


源码docs目录下包含与源码相关的很多说明,感兴趣可以下载来测试一下你MCU跑分多少。

其实,这里也有多种型号STM32的CoreMark跑分和测试结果:

5.jpg
6.jpg

STM32有些板子出厂就自带CoreMark测试程序,比如我这边手上有一个块STM32F429I-Discovery的板子,就自带性能测试代码。

7.jpg

移植的过程,我这里就不教大家了,官方提供一些例程,网上也有很多教程,感兴趣的小伙伴可以试着移植测试一下。


来源:strongerHuang

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

围观 25

通用异步收发器(Universal Asynchronous Receiver/Transmitter:UART),是一种通用串行数据总线,常用于系统内各子模块间的数据交换。

以CW32L083为例,CW32L083 内部集成 6 个通用异步收发器 (UART),支持异步全双工、同步半双工和单线半双工模式,支持硬件数据流控和多机通信;可编程数据帧结构,可以通过小数波特率发生器提供宽范围的波特率选择。UART 控制器工作在双时钟域下,允许在深度休眠模式下进行数据的接收,接收完成中断可以唤醒 MCU 回到运行模式。

一、主要功能

• 支持双时钟域驱动:配置时钟 PCLK;传输时钟 UCLK。

• 可编程数据帧结构:数据字长:8、9 位,LSB 在前;校验位:无校验、奇校验、偶校验;停止位长度:1、1.5、2 位 。

• 16 位整数、4 位小数波特率发生器 。

• 支持异步全双工、同步半双工、单线半双工 。

• 支持硬件流控 RTS、CTS。

• 支持直接内存访问 (DMA) 。

• 支持多机通信,自动地址识别 。

• 6 个带中断标志的中断源 。

• 错误检测:奇偶校验错误、帧结构错误 。

• 低功耗模式下收发数据,中断唤醒 MCU。

1.功能框图

UART 控制器挂载到 APB 总线上,配置时钟域 PCLK,固定为 APB 总线时钟 PCLK,用于寄存器配置逻辑工作;传输时钟域 UCLK,用于数据收发逻辑工作,其来源可选择 PCLK 时钟、外部低速时钟(LSE)以及内部低速时钟 (LSI)。双时钟域的设计更便于波特率的设置,支持从深度休眠模式下唤醒控制器。

1.png

2.UART中断

UART 控制器支持 6 个中断源,当 UART 中断触发事件发生时,中断标志位会被硬件置位,如果设置了对应的中断使能控制位,将产生中断请求。CW32L083 的一个 UART 模块使用一个系统 UART 中断,UART 中断是否产生中断跳转由嵌套向量中断控 制器 (NVIC) 的中断使能设置寄存器 NVIC_ISER 的相应位控制。系统 UART 中断示意图如下图所示:

2.png

在用户 UART 中断服务程序中,应查询相关 UART 中断标志位,以进行相应的处理,在退出中断服务程序之前, 要清除该中断标志位,避免重复进入中断程序。各 UART 中断源的标志位、中断使能位、中断标志清除位或清除方法,如下表所示:

3.png

3.CH340介绍

CH340是一个USB总线的转接芯片,实现USB协议和UART协议的自动转换。 

4.png

RTS#:MODEM联络输出信号,请求发送

UD+:直接连接USB总线的D+数据线

UD-:直接连接USB总线的D-数据线

V3:在3.3V电源电压时链接VCC输入外部电源,在5V电源电压时外接容量为100nF的退耦电容。

VCC:正电源输入端,需要接100nF电源退耦电容

TXD:串行电路输出

RXD:串行数据输入,内置可控上拉和下拉电阻

CH340内置了独立的收发缓冲区,支持单工、半双工或者全双工异步串行通讯。串行数据包括1个低电平起始位、5、6、7或8个数据位、1个或2个高电平停止位,支持奇校验/偶校验/标志校验/空白校验。CH340支持常用通讯波特率:50、75、100、110、134.5、150、300、600、900、1200、1800、2400、3600、4800、9600、14400、19200、28800、33600、38400、56000、57600、76800、115200、128000、153600、230400、460800、921600、1500000、2000000等。串口发送信号的波特率误差小于0.3%,串口接收信号的允许波特率误差不小于2%。

二、实例演示

本实例采用CW32L083V8T6的StartKit单板,MCU的串口引脚(PA08/ PA09)和CH340对接,CH340通过USB接口和PC机对接,实现PC机软件和MCU通过UART双向通信功能。

单板启动后,处于等待数据接收状态,当有数据接收到后,产生UART接收中断,在中断中读取接收到的数据,然后将数据通过UART再发送回来,并清除中断标志位,然后等待接收下一个数据。

1.配置RCC系统时钟

void RCC_Configuration(void)
{
    //SYSCLK = HSI = 8MHz = HCLK = PCLK
    RCC_HSI_Enable(RCC_HSIOSC_DIV6);
    //外设时钟使能
    RCC_AHBPeriphClk_Enable(DEBUG_UART_GPIO_CLK, ENABLE);
    DEBUG_UART_APBClkENx(DEBUG_UART_CLK, ENABLE);
}

2.GPIO配置

void GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    //UART TX RX 复用
    DEBUG_UART_AFTX;
    DEBUG_UART_AFRX;
    GPIO_InitStructure.Pins = DEBUG_UART_TX_GPIO_PIN;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_Init(DEBUG_UART_TX_GPIO_PORT, &GPIO_InitStructure);
    GPIO_InitStructure.Pins = DEBUG_UART_RX_GPIO_PIN;
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT_PULLUP;
    GPIO_Init(DEBUG_UART_RX_GPIO_PORT, &GPIO_InitStructure);
}

3.UART配置

void UART_Configuration(void)
{
    UART_InitTypeDef UART_InitStructure = {0};
    UART_InitStructure.UART_BaudRate = DEBUG_UART_BaudRate;
    UART_InitStructure.UART_Over = UART_Over_16;
    UART_InitStructure.UART_Source = UART_Source_PCLK;
    UART_InitStructure.UART_UclkFreq = DEBUG_UART_UclkFreq;
    UART_InitStructure.UART_StartBit = UART_StartBit_FE;
    UART_InitStructure.UART_StopBits = UART_StopBits_1;
    UART_InitStructure.UART_Parity = UART_Parity_No ;
    UART_InitStructure.UART_HardwareFlowControl = UART_HardwareFlowControl_None;
    UART_InitStructure.UART_Mode = UART_Mode_Rx | UART_Mode_Tx;
    UART_Init(DEBUG_UARTx, &UART_InitStructure);
}

4.配置NVIC

void NVIC_Configuration(void)
{
    //优先级,无优先级分组
    NVIC_SetPriority(DEBUG_UART_IRQ, 0);
    //UARTx中断使能
    NVIC_EnableIRQ(DEBUG_UART_IRQ);
}

5.中断函数处理UART2/UART5

void UART2_UART5_IRQHandler(void)
{
     /* USER CODE BEGIN */
    uint8_t TxRxBuffer;
    if(UART_GetITStatus(CW_UART5 UART_IT_RC) != RESET)// 获取UARTx中断标志位
    {
        TxRxBuffer = UART_ReceiveData_8bit(CW_UART5;// 通过UARTx接收一个数据(8bit)
        UART_SendData_8bit(CW_UART5 TxRxBuffer);// 通过UARTx发送一个数据(8bit)
        UART_ClearITPendingBit(CW_UART5 UART_IT_RC);// 清除UARTx中断标志位
    }
    /* USER CODE END */
}

6.定义常量define

//UARTx
#define  DEBUG_UARTx                   CW_UART5
#define  DEBUG_UART_CLK                RCC_APB1_PERIPH_UART5
#define  DEBUG_UART_APBClkENx          RCC_APBPeriphClk_Enable1
#define  DEBUG_UART_BaudRate           9600
#define  DEBUG_UART_UclkFreq           8000000
//UARTx GPIO
#define  DEBUG_UART_GPIO_CLK           RCC_AHB_PERIPH_GPIOB
#define  DEBUG_UART_TX_GPIO_PORT       CW_GPIOB
#define  DEBUG_UART_TX_GPIO_PIN        GPIO_PIN_8
#define  DEBUG_UART_RX_GPIO_PORT       CW_GPIOB
#define  DEBUG_UART_RX_GPIO_PIN        GPIO_PIN_9
//GPIO AF
#define  DEBUG_UART_AFTX               PB08_AFx_UART5TXD()
#define  DEBUG_UART_AFRX               PB09_AFx_UART5RXD()
//中断
#define  DEBUG_UART_IRQ                UART2_UART5_IRQn

7.UART中断方式接收数据

int32_t main(void)
{
     //配置RCC
    RCC_Configuration();
    //配置GPIO
    GPIO_Configuration();
    //配置UART
    UART_Configuration();
    //配置NVIC
     NVIC_Configuration();
    //使能UARTx RC中断
     UART_ITConfig(DEBUG_UARTx, UART_IT_RC, ENABLE);
    UART_SendString(DEBUG_UARTx, "\r\nCW32L083 UART Interrupt\r\n");
    while(1)
    {
        //中断收发
    }
}

8、测试结果如下:当MCU收到上位机发送的数据后,再回传到上位机,UART功能正常。

5.png

来源:武汉芯源半导体

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

围观 98

来源:https://gitee.com/wei513723/key_board

key_board介绍

key_board用于单片机中的小巧多功能按键支持,软件采用了分层的思想,并且做到了与平台无关,用户只需要提供按键的基本信息和读写io电平的函数即可,非常方便移植,同时支持多个矩阵键盘及多个单io控制键盘。

目前已实现按下触发、弹起触发、长按自动触发、长按弹起触发、多击触发、连续触发等功能,并且能够随意组合(支持状态的同一时间轴和非同一时间轴),后续还会添加更多的功能。

使用说明

1、初始化相关的硬件资源。

2、提供一个1ms的定时器,用于周期性的调用'key_check'函数。

3、提供按键的描述及读写io的函数。

4、将键盘注册到系统。

5、具体的操作参考提供的stm32例程。

6、因为程序默认使用了堆内存,当发现程序运行结果不正常时,尝试增大你的程序堆空间,或者注册调试接口查看原因。

7、更详细的使用教程见详细使用说明或者提供的stm32例程。

已支持的键盘

1、矩阵键盘

1.jpg

矩阵键盘

2、单io按键

2.jpg

单io按键

详细使用说明

将key_board.c,key_board.h,key_board_config.h放进key_board文件夹中并包含进你的工程,添加头文件路径。

基础功能移植(以stm32矩阵键盘为例)

首先需要一个可使用的定时器(如果不想使用定时器也可直接放到主循环中,但不推荐,会导致时基不准确),固定为1ms触发一次;

准备待检测的按键的基本信息,可参考key_board_sample.c文件中的struct key_pin_t结构体,如:

struct key_pin_t {
   GPIO_TypeDef *port;     //按键端口号
   uint16_t pin;           //按键的引脚号
   GPIO_PinState valid;    //按键的有效电平(即按键按下时的电平)
   GPIO_PinState invalid;  //按键的无效电平(即按键空闲时的电平)
   /*
   可添加你的其它参数
   */
};

定义待检测的按键信息,可参考key_board_sample.c文件中的const struct key_pin_t key_pin_sig[]结构体数组,对应头文件为key_board_sample.h,如:

//全局变量
const struct key_pin_t key_pin_sig[] = {
    {
       .port = KEY_PORT_J12,
       .pin = KEY_PIN_J12,
       .valid = KEY_PRESS_LEVEL_J12,
        .invalid = KEY_RELEASE_LEVEL_J12
  },
   {
       .port = KEY_PORT_J34,
       .pin = KEY_PIN_J34,
      .valid = KEY_PRESS_LEVEL_J34,
       .invalid = KEY_RELEASE_LEVEL_J34
  },
  {
      .port = KEY_PORT_J56,
       .pin = KEY_PIN_J56,
       .valid = KEY_PRESS_LEVEL_J56,
       .invalid = KEY_RELEASE_LEVEL_J56
  },
};

如果为矩阵键盘还需要定义控制io的相关信息,可参考key_board_sample.c文件中的const struct key_pin_t key_pin_ctrl[]结构体数组,对应头文件为key_board_sample.h,如:

const struct key_pin_t key_pin_ctrl[] = {
    {
       .port = KEY_PORT_J135,
       .pin = KEY_PIN_J135,
       .valid = KEY_CTL_LINE_ENABLE,
       .invalid = KEY_CTL_LINE_DISABLE
    },
    {
       .port = KEY_PORT_J246,
       .pin = KEY_PIN_J246,
       .valid = KEY_CTL_LINE_ENABLE,
       .invalid = KEY_CTL_LINE_DISABLE
    },
};

实现按键io的电平读取函数,可参考key_board_sample.c文件中的pin_level_get函数,如:

static inline bool pin_level_get(const void *desc)
{
  struct key_pin_t *pdesc;

  pdesc = (struct key_pin_t *)desc;
  return HAL_GPIO_ReadPin(pdesc->port, pdesc->pin) == pdesc->valid;
}

如果为矩阵键盘还需要实现按键io的电平写入函数,可参考key_board_sample.c文件中的pin_level_set函数,如:

static inline void pin_level_set(const void *desc, bool flag)
{
  struct key_pin_t *pdesc;

  pdesc = (struct key_pin_t *)desc;
  HAL_GPIO_WritePin(pdesc->port, pdesc->pin, flag ? pdesc->valid : pdesc->invalid);
}

定义按键的id及功能结构体struct key_public_sig_t,可参考key_board_sample.c文件中的const struct key_public_sig_t key_public_sig[]结构体数组,对应头文件key_board.h,如:

const struct key_public_sig_t key_public_sig[] = {
   KEY_PUBLIC_SIG_DEF(KEY_UP, &key_pin_sig[0], pin_level_get, KEY_FLAG_NONE),
   KEY_PUBLIC_SIG_DEF(KEY_LEFT, &key_pin_sig[1], pin_level_get, KEY_FLAG_NONE),
   KEY_PUBLIC_SIG_DEF(KEY_DOWN, &key_pin_sig[2], pin_level_get, KEY_FLAG_NONE),
   //下面的是因为使用的矩阵键盘而扩展出来的三个按键
   KEY_PUBLIC_SIG_DEF(KEY_ENTER, &key_pin_sig[0], pin_level_get, KEY_FLAG_NONE),
   KEY_PUBLIC_SIG_DEF(KEY_RIGHT, &key_pin_sig[1], pin_level_get, KEY_FLAG_NONE),
   KEY_PUBLIC_SIG_DEF(KEY_EXIT, &key_pin_sig[2], pin_level_get, KEY_FLAG_NONE),
};

如果为矩阵键盘还需要定义控制io的id及功能结构体struct key_public_ctrl_t,可参考key_board_sample.c文件中的const struct key_public_ctrl_t key_public_ctrl[]结构体数组,对应头文件key_board.h,如:

const struct key_public_ctrl_t key_public_ctrl[] = {
   KEY_PUBLIC_CTRL_DEF(&key_pin_ctrl[0], pin_level_set),
   KEY_PUBLIC_CTRL_DEF(&key_pin_ctrl[1], pin_level_set),
};

初始化键盘,可参考key_board_sample.c文件中的GPIO_Key_Board_Init函数,如:

void GPIO_Key_Board_Init(void)
{
   //硬件io的初始化
   GPIO_InitTypeDef GPIO_InitStruct;
   unsigned int i;

   RCC_KEY_BOARD_CLK_ENABLE();

   GPIO_InitStruct.Pull  = GPIO_PULLUP;
   GPIO_InitStruct.Mode  = GPIO_MODE_INPUT;
   for(i = 0;i < ARRAY_SIZE(key_pin_sig);i++)
   {
      GPIO_InitStruct.Pin   = key_pin_sig[i].pin;
      HAL_GPIO_Init(key_pin_sig[i].port, &GPIO_InitStruct);
   }

   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
   GPIO_InitStruct.Pull  = GPIO_NOPULL;
   GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
   for(i = 0;i < ARRAY_SIZE(key_pin_ctrl);i++)
   {
      GPIO_InitStruct.Pin   = key_pin_ctrl[i].pin;
      HAL_GPIO_Init(key_pin_ctrl[i].port, &GPIO_InitStruct);
   }

   //初始化键盘
   key_board_init();
   //注册键盘到系统中(矩阵键盘)
   key_board_register(KEY_BOARD_MATRIX, key_public_sig, ARRAY_SIZE(key_public_sig), key_public_ctrl, ARRAY_SIZE(key_public_ctrl));
}

主流程伪代码框架,更多例子参考main_test.c文件:

int main(void)
{
    //初始化硬件io,并注册键盘
    GPIO_Key_Board_Init();
    //初始化定时器,用于按键扫描(1ms)
    init_tmr();

    for(;;)
    {
        if(key_check_state(KEY_UP, KEY_RELEASE))
        {
            PRINTF("KEY_UP KEY_RELEASE\r\n");
        }
        if(key_check_state(KEY_UP, KEY_PRESS))
        {
            PRINTF("KEY_UP KEY_PRESS\r\n");
        }
    }
}

//定时器到期回调处理函数
void tmr_irq_callback(void)
{
    //调用按键扫描核心函数
    key_check();
}

扩展功能长按的使用

首先确保key_board_config.h文件中宏KEY_LONG_SUPPORT已处于使能状态,并且正确设置了宏KEY_DEFAULT_LONG_TRRIGER_TIME的值;

设置按键功能需要对长按进行检测,如:

KEY_PUBLIC_SIG_DEF(KEY_UP, &key_pin_sig[0], pin_level_get, KEY_FLAG_PRESS_LONG | KEY_FLAG_RELEASE_LONG)

使用例程:

if(key_check_state(KEY_UP, KEY_PRESS_LONG))
{
    PRINTF("KEY_UP KEY_PRESS_LONG\r\n");
}
if(key_check_state(KEY_UP, KEY_RELEASE_LONG))
{
    PRINTF("KEY_UP KEY_RELEASE_LONG\r\n");
}

扩展功能连按的使用

首先确保key_board_config.h文件中宏KEY_CONTINUOUS_SUPPORT已处于使能状态,并且正确设置了宏KEY_DEFAULT_CONTINUOUS_INIT_TRRIGER_TIME和KEY_DEFAULT_CONTINUOUS_PERIOD_TRRIGER_TIME的值;

设置按键功能需要对连按进行检测,如:

KEY_PUBLIC_SIG_DEF(KEY_UP, &key_pin_sig[0], pin_level_get, KEY_FLAG_PRESS_CONTINUOUS)

使用例程:

if(key_check_state(KEY_UP, KEY_PRESS_CONTINUOUS))
{
    PRINTF("KEY_UP KEY_PRESS_CONTINUOUS\r\n");
}

扩展功能多击的使用

首先确保key_board_config.h文件中宏KEY_MULTI_SUPPORT已处于使能状态,并且正确设置了宏KEY_DEFAULT_MULTI_INTERVAL_TIME的值;

设置按键功能需要多击进行检测,如:

KEY_PUBLIC_SIG_DEF(KEY_UP, &key_pin_sig[0], pin_level_get, KEY_FLAG_PRESS_MULTI | KEY_FLAG_RELEASE_MULTI)

使用例程:

unsigned int res;
res = key_check_state(KEY_UP, KEY_PRESS_MULTI);
if(res)
{
    PRINTF("KEY_UP KEY_PRESS_MULTI:%d\r\n", res);
}
res = key_check_state(KEY_UP, KEY_RELEASE_MULTI);
if(res)
{
    PRINTF("KEY_UP KEY_RELEASE_MULTI:%d\r\n", res);
}

扩展功能组合状态(同一时间轴)

感谢网友:石玉虎[@shi-yuhu]的反馈,已更正之前错误的使用案例。

使用例程:

unsigned int key_down_release_long, key_up_release_long;
key_down_release_long = key_check_state(KEY_DOWN, KEY_RELEASE_LONG);
key_up_release_long = key_check_state(KEY_UP, KEY_RELEASE_LONG);
if(key_down_release_long && key_up_release_long)
{
    PRINTF("KEY_DOWN KEY_RELEASE_LONG && KEY_UP KEY_RELEASE_LONG\n");
}

扩展功能组合状态(非同一时间轴)

首先确保key_board_config.h文件中宏KEY_COMBINE_SUPPORT已处于使能状态,并且正确设置了宏KEY_DEFAULT_COMBINE_INTERVAL_TIME的值;

使用例程:

//用于保存注册后的组合状态id
static unsigned int test_id1, test_id2;

//定义要检测的状态
const struct key_combine_t test_combine1[] = {
    { .id = KEY_UP,     .state = KEY_PRESS },
    { .id = KEY_DOWN,   .state = KEY_PRESS_LONG },
    { .id = KEY_UP,     .state = KEY_PRESS },
};
//注册组合状态
test_id1 = key_combine_register(test_combine1, ARRAY_SIZE(test_combine1));

const struct key_combine_t test_combine2[] = {
    { .id = KEY_UP,     .state = KEY_PRESS },
    { .id = KEY_DOWN,   .state = KEY_PRESS },
    { .id = KEY_UP,     .state = KEY_PRESS },
    { .id = KEY_DOWN,   .state = KEY_PRESS },
};
test_id2 = key_combine_register(test_combine2, ARRAY_SIZE(test_combine2));

if(key_check_combine_state(test_id1))
{
    PRINTF("combine test_id1\r\n");
}

if(key_check_combine_state(test_id2))
{
    PRINTF("combine test_id2\r\n");
}

来源:https://gitee.com/wei513723/key_board

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

围观 34

一、概述

CW32L083 内部集成 2 个 I2C 控制器,能按照设定的传输速率(标准,快速,高速)将需要发送的数据按照 I2C 规范串行发送到 I2C 总线上,或从总线上接收数据,并对通信过程中的状态进行检测,另外还支持多主机通信中的总线冲突和仲裁处理。

二、主要功能

• 支持主机发送 / 接收,从机发送 / 接收四种工作模式 

• 支持时钟延展 ( 时钟同步 ) 和多主机通信冲突仲裁

• 支持标准 (100Kbps)/ 快速 (400Kbps)/ 高速 (1Mbps) 三种工作速率 

• 支持 7bit 寻址功能 

• 支持 3个从机地址 

• 支持广播地址 

• 支持输入信号噪声过滤功能 

• 支持中断状态查询功能

1.协议介绍

I2C 总线使用两根信号线(数据线 SDA 和时钟线 SCL)在设备间传输数据。SCL 为单向时钟线,固定由主机驱动。SDA 为双向数据线,在数据传输过程中由收发两端分时驱动。I2C 总线上可以连接多个设备,所有设备在没有进行数据传输时都处于空闲状态(未寻址从机接收模式),任一设备都可以作为主机发送 START 起始信号来开始数据传输,在 STOP 停止信号出现在总线上之前,总线一直处于 被占用状态。I2C 通信采用主从结构,并由主机发起和结束通信。主机通过发送 START 起始信号来发起通信,之后发送 SLA+W/R 共 8bit 数据(其中,SLA 为 7bit 从机地址,W/R 为读写位),并在第 9个SCL 时钟释放 SDA 总线, 对应的从机在第 9个SCL 时钟占用 SDA 总线并输出 ACK 应答信号,完成从机寻址。此后根据主机发送的第 1 字 节的 W/R 位来决定数据通信的发端和收端,发端每发送 1个字节数据,收端必须回应 1个ACK 应答信号。数据传输完成后,主机发送 STOP 信号结束本次通信。

2.功能框图

I2C 模块主要包括时钟发生器、输入滤波器、地址比较器、协议控制逻辑、仲裁和同步逻辑、以及相关寄存器等。

1.png

CW32L083 支持用户灵活选择 GPIO 作为 I2C 通信引脚,如下表所示:

2.png

3.I2C中断

I2C 控制寄存器 I2Cx_CR 的 SI 位域为中断标志位。当 I2C 状态寄存器 I2Cx_STAT 的 STAT 位域值发生改变(变成 0xF8 除外)时,I2Cx_CR.SI 标志位就会被置位,同时产生中断请求。在用户 I2C 中断服务程序中,应查询 I2C 状态寄存器 I2Cx_STAT 的 STAT 位域值获取 I2C 总线的状态,以确定中断产生原因。设置 I2Cx_CR.SI 为 0 清除该标志位。

4.工作模式

I2C 控制器支持 4 种工作模式:主机发送模式、主机接收模式、从机发送模式、从机接收模式。另外还支持广播 接收模式,其工作方式和从机接收模式类似。

三、EEPROM(CW24C02AD)

1.功能简介

CW24C02是一个2Kbit的串行EEPROM存储芯片,可存储256个字节数据。芯片内部分为32页,每页8字节。工作电压范围为1.7V到5.5V,I2C接口时钟频率为 1MHz (5V,3V),400 KHz (1.7V)。器件地址为1010 A2 A1 A0,对于我们单板A2 A1 A0引脚全部接GND,故器件地址为1010000,即0x50。器件内部存储空间地址长度8 bit。

2.读写时序

字节写操作时序:起始信号+器件地址(7bit)+读写指示(1bit)+存储空间地址(8bit)+写入数据(8bit)+停止信号,即可完成指定字节写入操作。

3.png

页写操作时序:起始信号+器件地址(7bit)+读写指示(1bit)+存储空间地址(8bit)+写入数据(8bit*8)+停止信号,即可完成指定地址(必须是页起始地址)的页写入操作。

4.png

随机读操作时序:起始信号+器件地址(7bit)+读写指示(1bit)+存储空间地址(8bit)+重复起始信号+器件地址(7bit)+读写指示(1bit),之后器件会返回1字节数据,主机收到后发送停止信号,即可完成指定字节读取操作。

5.png

顺序读操作时序:和随机读时序类似,只是在主机接收到1字节数据后,不发送停止信号,而是继续发送时钟进行下一个字节数据的接收,直到所有所需读取的数据全部读取,之后再发送停止信号。

四、硬件连接

如下图所示,MCU和EEPROM通过I2C总线互连。

6.png

五、实例演示:MCU采用页写和顺序读操作时序完成EERPOM的访问。

1.I2C读写EEPROM芯片中断函数(I2C分为I2C1和I2C2)

void I2c1EepromReadWriteInterruptFunction(void)
{
    u8State = I2C_GetState(CW_I2C1);// I2C:获取状态寄存器函数
    switch(u8State)
    {
        case 0x08:     //发送完START信号
        I2C_GenerateSTART(CW_I2C1, DISABLE);// 发送START信号
        I2C_Send7bitAddress(CW_I2C1, I2C_SLAVEADDRESS,0X00);// 做主时发送从机地址字节
        break;
        case 0x10:     //发送完重复起始信号
        I2C_GenerateSTART(CW_I2C1, DISABLE);
        if(0 == SendFlg)
        {
            I2C_Send7bitAddress(CW_I2C1, I2C_SLAVEADDRESS,0X00);    //写命令
        }
        else
        {
            I2C_Send7bitAddress(CW_I2C1, I2C_SLAVEADDRESS,0X01); //读命令,eeprom 随机读
        }
        break;
        case 0x18:    //发送完SLA+W/R字节
        I2C_GenerateSTART(CW_I2C1, DISABLE);
        I2C_SendData(CW_I2C1, u8Addr);   //发送访问EEPROM的目标地址字节
        break;
        case 0x20:    //发送完SLA+W后从机返回NACK
        case 0x38:    //主机在发送 SLA+W 阶段或者发送数据阶段丢失仲裁  或者  主机在发送 SLA+R 阶段或者回应 NACK 阶段丢失仲裁
        case 0x30:    //发送完一个数据字节后从机返回NACK
        case 0x48:    //发送完SLA+R后从机返回NACK
        I2C_GenerateSTOP(CW_I2C1, ENABLE);
        I2C_GenerateSTART(CW_I2C1, ENABLE);
        break;
        case 0x58:    //接收到一个数据字节,且NACK已回复
        u8Recdata[u8RecvLen++] = I2C_ReceiveData(CW_I2C1);//所有数据读取完成,NACK已发送
        receivedflag =1;
        I2C_GenerateSTOP(CW_I2C1, ENABLE);//发送停止条件
        break;
        case 0x28:     //发送完1字节数据:发送EEPROM中memory地址也会产生,发送后面的数据也会产生
        if(0 == SendFlg)
        {
            if(u8SendLen <WRITELEN)
            {
                 I2C_SendData(CW_I2C1,u8Senddata[u8SendLen++]);
            }
            else
            {
                u8SendLen = 0;
                Comm_flg = 1;
                SendFlg = 1;
                I2C_GenerateSTOP(CW_I2C1, ENABLE);//发送完数据,发送停止信号
            }
        }
        else//SendFlg=1为读,SendFlg=0为写。读数据发送完地址字节后,重复起始条件
        {
            CW_I2C1->CR_f.STA = 1;  //set start       //发送重复START信号,START生成函数改写后,会导致0X10状态被略过,故此处不调用函数
            I2C_GenerateSTOP(CW_I2C1, DISABLE);
        }
        break; 
        case 0x40:     //发送完SLA+R信号,开始接收数据
        u8RecvLen = 0;
        if(READLEN>1)
        {
            I2C_AcknowledgeConfig(CW_I2C1,ENABLE);//读取数据超过1个字节才发送ACK
        }
        break;
        case 0x50:     //接收完一字节数据,在接收最后1字节数据之前设置AA=0;
        u8Recdata[u8RecvLen++] = I2C_ReceiveData(CW_I2C1);
        if(u8RecvLen==READLEN-1)
        {
            I2C_AcknowledgeConfig(CW_I2C1,DISABLE);;
        }
        break;
    }
    I2C_ClearIrq(CW_I2C1);
}

2.设置系统时钟

void RCC_Configuration(void)
{
    CW_SYSCTRL->APBEN1_f.I2C1 = 1U;   
}

3.设置GPIO口

void GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    CW_SYSCTRL->AHBEN_f.GPIOA  = 1;
    CW_SYSCTRL->AHBEN_f.GPIOB  = 1;
    CW_SYSCTRL->AHBEN_f.GPIOC  = 1;
    CW_SYSCTRL->AHBEN_f.GPIOD  = 1;
    CW_SYSCTRL->AHBEN_f.GPIOE  = 1;
    CW_SYSCTRL->AHBEN_f.GPIOF  = 1;
    
    PB10_AFx_I2C1SCL();
    PB11_AFx_I2C1SDA();
    GPIO_InitStructure.Pins = I2C1_SCL_GPIO_PIN | I2C1_SDA_GPIO_PIN;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_Init(I2C1_SCL_GPIO_PORT, &GPIO_InitStructure);
}

4.配置嵌套矢量中断控制器

void NVIC_Configuration(void)
{
     __disable_irq();
    NVIC_EnableIRQ(I2C1_IRQn);
    __enable_irq();
}
void I2C1_IRQHandler(void)
{
    I2c1EepromReadWriteInterruptFunction();
}

5.定义常量

#define  I2C1_SCL_GPIO_PORT       CW_GPIOB
#define  I2C1_SCL_GPIO_PIN        GPIO_PIN_10   
#define  I2C1_SDA_GPIO_PORT       CW_GPIOB
#define  I2C1_SDA_GPIO_PIN        GPIO_PIN_11    
//EEPROM内部地址
uint8_t u8Addr = 0x00;        //地址字节
#define WRITELEN   8          //写数据长度
#define READLEN   8           //读数据长度
#define WriteReadCycle  35    //写读次数,每次写入数据为n+i(n为次数,i=0~7)
uint8_t u8Senddata[8] = {0x66,0x02,0x03,0x04,0x05,0x60,0x70,0x20};
uint8_t u8Senddata2[8] = {0x55,0xAA,0xAA,0x55,0x55,0xAA,0x55,0xAA};
uint8_t u8Recdata[16]= {0x00};
uint8_t u8SendLen=0;
uint8_t u8RecvLen=0;
uint8_t SendFlg = 0,Comm_flg = 0;
uint8_t u8recvflg=0;
uint8_t u8State = 0;
uint8_t receivedflag = 0;    //读取完成标志

6.主程序:利用I2C接口,采用中断方式读写EEPROM芯片(CW24C02)。

int32_t main(void)
{
    I2C_InitTypeDef I2C_InitStruct = {0};
    uint16_t tempcnt = 0 ;
    RCC_Configuration();//时钟初始化
    GPIO_Configuration();//IO口初始化
    //I2C初始化
    I2C_InitStruct.I2C_Baud = 0x01;//500K=(8000000/(8*(1+1)) ,波特率计数器配置
    I2C_InitStruct.I2C_BaudEn = ENABLE;// 波特率计数器使能
    I2C_InitStruct.I2C_FLT = DISABLE; //<FLT配置
    I2C_InitStruct.I2C_AA =  DISABLE; //<ACK配置
    I2C1_DeInit();
    I2C_Master_Init(CW_I2C1,&I2C_InitStruct);//初始化模块
    NVIC_Configuration();//中断设置
    //I2C模块开始工作
    I2C_Cmd(CW_I2C1,ENABLE);  //模块使能
    tempcnt =0;
    for(uint8_t i=0; i<8; i++)
    {
        u8Senddata[i] = i;
    }
    while(1)
    {
        I2C_GenerateSTART(CW_I2C1, ENABLE); //开始信号
        while(1)
        {
            while(!Comm_flg) ; //等待数据发送完成
            FirmwareDelay(3000);
            
            Comm_flg = 0; //启动读数据过程
            receivedflag=0;
            I2C_GenerateSTART(CW_I2C1, ENABLE); //开始信号
            while(!receivedflag) ; //等待数据读取完成
            receivedflag = 0; //初始化下一次写数据
            SendFlg = 0;
            u8RecvLen = 0;
            tempcnt++;
            for(uint8_t i=0; i<8; i++)
            {
                u8Senddata[i] =tempcnt+i;
            }
            break;
        }

        if(tempcnt >=WriteReadCycle) //测试次数完成,退出
        {
            break;
        }
    }
    while(1);
}

7.程序流程

程序完成I2C主设备配置后,先将u8Senddata数组中的内容写入到EEPROM的第1页(CW24C02每页8字节):发送START信号后,I2C模块会产生状态改变中断,在中断服务程序中根据不同状态值进行不同处理,直到完成CW24C02的页写模式所有数据字节以及STOP信号发送,发送完成后置写操作流程完成标志。主循环中判断到写操作流程完成后,启动从EERROM的第1页数据读取流程:发送启动信号后,I2C模块会产生状态改变中断,在中断服务程序中根据不同状态值进行不同处理,直到完成CW24C02的顺序读模式所有数据字节发送及读取,在发送完STOP信号后置读操作流程完成标志。主循环中判断读操作流程完成后,初始化u8Senddata数组内容,重复下一次测试过程。完成WriteReadCycle变量设置的测试次数后退出。

来源:武汉芯源半导体

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

围观 34

一、前言

在嵌入式产品开发中,难以避免地会因为各种原因导致最后出货的产品存在各种各样的BUG,通常会给产品进行固件升级来解决问题。记得之前在公司维护一款BLE产品的时候,由于前期平台预研不足,OTA参数设置不当,导致少数产品出现不能OTA的情况,经过分析只需改变代码中的某个参数数值即可,但是产品在用户手里,OTA是唯一能更新代码的方式,只能给用户重发产品。后来在想,是否可以提前做好一个接口,支持动态地传输少量代码到产品中临时运行,通过修改特定位置的Flash代码数据来修复产品的棘手BUG?多留一个后门,有时候令产品出棘手问题的往往是那么一两行代码或者几个初始化的参数不对,那么这种方法也可以应应急,虽然操作比较骚。

二、创建演示工程

本文以STM32F103C8T6单片机为例创建演示工程,分为app和bootloader两个工程。即将mcu的Flash分为“app”和“bootloader”两个区域, bootloader放在0x8000000为起始的24KB区域内,app放在0x8006000为起始的后续区域。bootloader完成对app的Flash数据修改。

1、app工程

注意app的工程需要在keil上修改ROM起始地址。

1.png

还要在app代码的开头设置向量偏移(调用一行代码):

NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x6000);

app工程的逻辑为:先顺序执行3个不同速度的LED闪灯过程(20ms、200ms、500ms、切换亮灭),最后进入到一个循环状态每秒切换一次LED的状态闪烁。代码如下:

void init_led(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;     
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;     
    GPIO_Init(GPIOB, &GPIO_InitStructure);    

    GPIO_ResetBits(GPIOB, GPIO_Pin_10);
    GPIO_SetBits(GPIOB, GPIO_Pin_10); 
}

void led_blings_1(void)
{
    uint32_t i;

    for (i = 0; i < 10; i++)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(20);  

        GPIO_ResetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(20);
    }
}

void led_blings_2(void)
{
    uint32_t i;

    for (i = 0; i < 10; i++)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(200);  

        GPIO_ResetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(200);
    }
}

void led_blings_3(void)
{
    uint32_t i;

    for (i = 0; i < 10; i++)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(500);  

        GPIO_ResetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(500);
    }
}

int main()
{
    NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x6000);

    SysTick_Init(72);

    init_led();

    led_blings_1();
    led_blings_2();
    led_blings_3();

    while (1)
    {
        GPIO_SetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(1000);  

        GPIO_ResetBits(GPIOB, GPIO_Pin_10); 
        delay_ms(1000);
    }
}

为了分析汇编和查看bin文件数据,我们需要在keil中添加两条命令,分别生成.dis反汇编和.bin的代码文件。(具体的目录情况依葫芦画瓢)

fromelf --text -a -c --output=all.dis Obj\Template.axf

fromelf --bin --output=test.bin Obj\Template.axf

2.png

先将app的代码烧写进单片机,注意烧写设置里面选择“Erase Sectors”只擦除需要烧写的地方。

2、bootloader工程

在bootloader中分为两部分,不变的代码部分和变动的代码部分(error_process函数)。初次编译的时候error_process写为空函数,当我们有需求对App进行修改的时候,我们重新编译工程对error_process函数进行填充。为了重新编译工程的时候不影响之前函数的链接地址,特意将error_process函数放到代码区的最后0x8000800地址处,理由是原来工程大小是1.51KB,擦除页大小是2KB,所以需要2KB对齐,对齐处的地址就选择0x8000800为起始。代码如下:

#define FLASH_PAGE_SIZE 2048
#define ERROR_PROCESS_CODE_ADDR 0x8000800

void error_process(void) __attribute__((section(".ARM.__at_0x8000800")));

void init_led(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;     
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;     
    GPIO_Init(GPIOB, &GPIO_InitStructure);    

    GPIO_ResetBits(GPIOB, GPIO_Pin_10);
    GPIO_SetBits(GPIOB, GPIO_Pin_10); 
}

uint32_t pageBuf[FLASH_PAGE_SIZE / 4];

void error_process(void)
{

}

void eraseErrorProcessCode(void)
{
    FLASH_Unlock();
    FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | 
                    FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
    FLASH_ErasePage(ERROR_PROCESS_CODE_ADDR);
    FLASH_Lock();
}

void(*boot_jump2App)();

void boot_loadApp(uint32_t addr)
{
    uint8_t i;

    if (((*(vu32*)addr) & 0x2FFE0000) == 0x20000000)    
    {
        boot_jump2App = (void(*)())*(vu32*)(addr + 4);      

        __set_MSP(*(vu32*)addr);

        for (i = 0; i < 8; i++)
        {
            NVIC->ICER[i] = 0xFFFFFFFF; 
            NVIC->ICPR[i] = 0xFFFFFFFF; 
        }

        boot_jump2App();        

        while (1);
    }
}

int main()
{
    uint32_t flag;

    SysTick_Init(72);

    flag = *((uint32_t *)ERROR_PROCESS_CODE_ADDR);

    if ((flag != 0xFFFFFFFF) && (flag != 0))
    {
        init_led();
        GPIO_ResetBits(GPIOB, GPIO_Pin_10); 

        delay_ms(1000);
        delay_ms(1000);

        error_process();
        eraseErrorProcessCode();
    }

    boot_loadApp(0x8006000);

    while (1);
}

一进main函数就读取0x8000800地址处的32位数据,如果不是全F或者全0那么这个地方是有函数体存在需要执行的,那么将LED亮起2秒钟代表bootloader识别到有处理程序需要执行(当然这里还需要加一些error_process代码数据是否完整之类的判断机制,这里演示先略去)。执行完处理程序后将处理程序擦除(数据变为全F),避免以后每次上电都重复擦写Flash。

error_process函数代码的数据由产品正常使用期间通过数据接口传入直接写入到0x8000800处(这部分的demo略去),编译后查看生成的bin文件将error_process部分的代码截取出来传输到Flash地址0x8000800处。

bootloader的代码烧写进单片机时注意烧写设置里面选择“Erase Sectors”只擦除需要烧写的地方。keil设置里ROM地址改回0x08000000。

三、修改app的特定参数

在app的工程中以“led_blings_1”函数为例,反汇编如下:

    $t
    i.led_blings_1
    led_blings_1
        0x08006558:    b510        ..      PUSH     {r4,lr}
        0x0800655a:    2400        .$      MOVS     r4,#0
        0x0800655c:    e010        ..      B        0x8006580 ; led_blings_1 + 40
        0x0800655e:    f44f6180    O..a    MOV      r1,#0x400
        0x08006562:    4809        .H      LDR      r0,[pc,#36] ; [0x8006588] = 0x40010c00
        0x08006564:    f7fffea2    ....    BL       GPIO_SetBits ; 0x80062ac
        0x08006568:    2014        .       MOVS     r0,#0x14
        0x0800656a:    f7ffffaf    ....    BL       delay_ms ; 0x80064cc
        0x0800656e:    f44f6180    O..a    MOV      r1,#0x400
        0x08006572:    4805        .H      LDR      r0,[pc,#20] ; [0x8006588] = 0x40010c00
        0x08006574:    f7fffe98    ....    BL       GPIO_ResetBits ; 0x80062a8
        0x08006578:    2014        .       MOVS     r0,#0x14
        0x0800657a:    f7ffffa7    ....    BL       delay_ms ; 0x80064cc
        0x0800657e:    1c64        d.      ADDS     r4,r4,#1
        0x08006580:    2c0a        .,      CMP      r4,#0xa
        0x08006582:    d3ec        ..      BCC      0x800655e ; led_blings_1 + 6
        0x08006584:    bd10        ..      POP      {r4,pc}
    $d
        0x08006586:    0000        ..      DCW    0
        0x08006588:    40010c00    ...@    DCD    1073810432

由于led是20ms交替亮灭一次,如果我们觉得这个参数有问题想改成100ms,从汇编上来说就是要改变两行代码:

    0x08006568:    2014        .       MOVS     r0,#0x14

    0x08006578:    2014        .       MOVS     r0,#0x14

    改为

    0x08006568:    2064        2       MOVS     r0,#0x64

    0x08006578:    2064        2       MOVS     r0,#0x64

bootloader工程中error_process的函数实现如下:

void error_process(void)
{
    #define MODIFY_FUNC_ADDR_START 0x08006558

    uint32_t alignPageAddr = MODIFY_FUNC_ADDR_START / FLASH_PAGE_SIZE * FLASH_PAGE_SIZE;
    uint32_t cnt, i;

    // 1. copy old code
    memcpy(pageBuf, (void *)alignPageAddr, FLASH_PAGE_SIZE);

    // 2. change code.
        //由于Flash操作2KB页的特性,0x08006558不满2kb,因此偏移为0x558,0x558/4=342
    pageBuf[90 + 256] = (pageBuf[90 + 256] & 0xFFFF0000) | 0x2064;
    pageBuf[94 + 256] = (pageBuf[94 + 256] & 0xFFFF0000) | 0x2064;

    // 3. erase old code, copy new code.
    FLASH_Unlock();
    FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | 
                    FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
    FLASH_ErasePage(alignPageAddr);

    cnt = FLASH_PAGE_SIZE / 4;
    for (i = 0; i < cnt; i++)
    {
        FLASH_ProgramWord(alignPageAddr + i * 4, pageBuf[i]);
    }

    FLASH_Lock();
}

由于Flash的2KB页擦除特性,这里先将待修改代码区的Flash页数据拷贝到缓冲buffer里,然后修改buffer里的数据,之后擦除Flash相关页,最后将buffer里修改后的数据重新写回到Flash里去。error_process函数的反汇编如下:

    $t
    .ARM.__at_0x8000800
    error_process
        0x08000800:    b570        p.      PUSH     {r4-r6,lr}
        0x08000802:    4d1a        .M      LDR      r5,[pc,#104] ; [0x800086c] = 0x8006000
        0x08000804:    142a        *.      ASRS     r2,r5,#16
        0x08000806:    4629        )F      MOV      r1,r5
        0x08000808:    4819        .H      LDR      r0,[pc,#100] ; [0x8000870] = 0x20000008
        0x0800080a:    f7fffcbd    ....    BL       __aeabi_memcpy ; 0x8000188
        0x0800080e:    4818        .H      LDR      r0,[pc,#96] ; [0x8000870] = 0x20000008
        0x08000810:    f8d00568    ..h.    LDR      r0,[r0,#0x568]
        0x08000814:    f36f000f    o...    BFC      r0,#0,#16
        0x08000818:    f2420164    B.d.    MOV      r1,#0x2064
        0x0800081c:    4408        .D      ADD      r0,r0,r1
        0x0800081e:    4914        .I      LDR      r1,[pc,#80] ; [0x8000870] = 0x20000008
        0x08000820:    f8c10568    ..h.    STR      r0,[r1,#0x568]
        0x08000824:    4608        .F      MOV      r0,r1
        0x08000826:    f8d00578    ..x.    LDR      r0,[r0,#0x578]
        0x0800082a:    f36f000f    o...    BFC      r0,#0,#16
        0x0800082e:    f2420164    B.d.    MOV      r1,#0x2064
        0x08000832:    4408        .D      ADD      r0,r0,r1
        0x08000834:    490e        .I      LDR      r1,[pc,#56] ; [0x8000870] = 0x20000008
        0x08000836:    f8c10578    ..x.    STR      r0,[r1,#0x578]
        0x0800083a:    f7fffd53    ..S.    BL       FLASH_Unlock ; 0x80002e4
        0x0800083e:    2035        5       MOVS     r0,#0x35
        0x08000840:    f7fffcca    ....    BL       FLASH_ClearFlag ; 0x80001d8
        0x08000844:    4628        (F      MOV      r0,r5
        0x08000846:    f7fffccd    ....    BL       FLASH_ErasePage ; 0x80001e4
        0x0800084a:    14ae        ..      ASRS     r6,r5,#18
        0x0800084c:    2400        .$      MOVS     r4,#0
        0x0800084e:    e007        ..      B        0x8000860 ; error_process + 96
        0x08000850:    4a07        .J      LDR      r2,[pc,#28] ; [0x8000870] = 0x20000008
        0x08000852:    f8521024    R.$.    LDR      r1,[r2,r4,LSL #2]
        0x08000856:    eb050084    ....    ADD      r0,r5,r4,LSL #2
        0x0800085a:    f7fffd0d    ....    BL       FLASH_ProgramWord ; 0x8000278
        0x0800085e:    1c64        d.      ADDS     r4,r4,#1
        0x08000860:    42b4        .B      CMP      r4,r6
        0x08000862:    d3f5        ..      BCC      0x8000850 ; error_process + 80
        0x08000864:    f7fffcfe    ....    BL       FLASH_Lock ; 0x8000264
        0x08000868:    bd70        p.      POP      {r4-r6,pc}
    $d
        0x0800086a:    0000        ..      DCW    0
        0x0800086c:    08006000    .`..    DCD    134242304
        0x08000870:    20000008    ...     DCD    536870920

那么这124个字节就是最终要传输到0x8000800处的函数数据。传输完毕后软复位mcu,bootloader将app的Flash数据进行篡改,达到改变程序功能的目的。

为什么要在bootloader运行时篡改app的数据?按理说在app运行时接收到error_process函数的更新数据后可以立刻运行,但是由于涉及到对app自身代码的修改,涉及Flash修改的一些相关函数有可能会被暂时破坏而导致代码运行崩溃。

四、跳过app的某些函数

如果想跳过“led_blings_1”函数,有2种方法:

1、函数内部跳过

即将以下汇编语句

    0x0800655a:    2400        .$      MOVS     r4,#0

    修改为

    0x0800655a:    e013        .$      B             0x08006584

在“led_blings_1”函数入口处指令修改直接跳转到函数出口处。至于汇编的机器码和用法文末有相关资料可以查阅。

因为修改处的字节偏移为0x55a,是pageBuf下标为342元素的高2Byte,需要在error_process函数中做如下修改:

pageBuf[342] = (pageBuf[342] & 0x0000FFFF) | 0xe0130000;

2、函数调用处跳过

main函数汇编如下:

    $t
    i.main
    main
        0x080065f8:    f44f41c0    O..A    MOV      r1,#0x6000
        0x080065fc:    f04f6000    O..`    MOV      r0,#0x8000000
        0x08006600:    f7fffe5c    ..\.    BL       NVIC_SetVectorTable ; 0x80062bc
        0x08006604:    2048        H       MOVS     r0,#0x48
        0x08006606:    f7ffff01    ....    BL       SysTick_Init ; 0x800640c
        0x0800660a:    f7ffff85    ....    BL       init_led ; 0x8006518
        0x0800660e:    f7ffffa3    ....    BL       led_blings_1 ; 0x8006558
        0x08006612:    f7ffffbb    ....    BL       led_blings_2 ; 0x800658c
        0x08006616:    f7ffffd3    ....    BL       led_blings_3 ; 0x80065c0
        0x0800661a:    e011        ..      B        0x8006640 ; main + 72
        0x0800661c:    f44f6180    O..a    MOV      r1,#0x400
        0x08006620:    4808        .H      LDR      r0,[pc,#32] ; [0x8006644] = 0x40010c00
        0x08006622:    f7fffe43    ..C.    BL       GPIO_SetBits ; 0x80062ac
        0x08006626:    f44f707a    O.zp    MOV      r0,#0x3e8
        0x0800662a:    f7ffff4f    ..O.    BL       delay_ms ; 0x80064cc
        0x0800662e:    f44f6180    O..a    MOV      r1,#0x400
        0x08006632:    4804        .H      LDR      r0,[pc,#16] ; [0x8006644] = 0x40010c00
        0x08006634:    f7fffe38    ..8.    BL       GPIO_ResetBits ; 0x80062a8
        0x08006638:    f44f707a    O.zp    MOV      r0,#0x3e8
        0x0800663c:    f7ffff46    ..F.    BL       delay_ms ; 0x80064cc
        0x08006640:    e7ec        ..      B        0x800661c ; main + 36
    $d
        0x08006642:    0000        ..      DCW    0
        0x08006644:    40010c00    ...@    DCD    1073810432

下面是调用语句

0x0800660e:    f7ffffa3    ....    BL       led_blings_1 ; 0x8006558

直接将此语句改为空语句nop(0xbf00)即可跳过调用,由于该命令占用4个字节,nop是两个字节的命令,所以替换为两个nop命令。

0x0800660e:    bf00bf00    ....    NOP

因为修改处的字节偏移为0x60e,是pageBuf下标为387元素的高2Byte和下标为388元素的低2Byte,需要在error_process函数中做如下修改:

pageBuf[387] = (pageBuf[387] & 0x0000FFFF) | 0xbf000000; 
pageBuf[388] = (pageBuf[388] & 0xFFFF0000) | 0x0000bf00;

来源:嵌入式基地

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

围观 36

减少后级电源对前级的影响,防止电源正负接反烧坏后级电路,防止电源关电时电流倒灌,但经过二极管有0.4V左右压降,需要考虑经过0.4V降压后会不会低于后级电路的正常工作电压。

一、按键电路

R1上拉电阻:

1.png

将不确定的信号通过一个电阻钳位在高电平,维持在不被触发的状态或是触发后回到原状态。(个人建议加上)

C1电容:

减小按键抖动及高频信号干扰。(个人建议加上)

R2限流电阻:

保护IO口,防止过流过高电压烧坏IO口,对静电或者一些高压脉冲有吸收作用。(个人建议加上)

R2的取值100欧~10k不等,如果有设置内部上拉,该值不能太大,否则电流不足以拉低IO口。

D1 ESD二极管:

静电保护二极管,防止静电干扰或者损坏IO口。(这个根据PCB的成本及防护级别要求来决定添加与否)

二、外接信号输入

2.png

R3上拉电阻:

将不确定的信号通过一个电阻钳位在高电平,维持在不被触发的状态或是触发后回到原状态。(如果外接的连接线比较长,芯片内部上拉能力比较弱,则建议加上。平时通信距离不长,有内部上拉则可以省略)

C2电容:

防止高频信号干扰。(注意,如果输入频率信号比较大,C2容值要对应减少,或者直接省略C2)

R4限流电阻:

保护IO口,防止过流过高电压烧坏IO口,对静电或者一些高压脉冲有吸收作用。(个人建议加上)

D2 ESD二极管:静电保护二极管,防止静电干扰或者损坏IO口。(这个根据PCB的成本及防护级别要求来决定添加与否)

三、输出电路继电器

3.png

U1光耦:

分离高低压,防止高压干扰,实现电气隔离。

D5 1N4148:

续流二极管,保护元件不被感应电压击du穿或烧坏,以并联的方式接到产生感应电动势的元件两端,并与其形成回路,使其产生的高电动势在回路以续电流方式消耗,从而起到保护电路中的元件不被损坏的作用。

四、达林顿晶体管

4.png

达林顿晶体管,小伙伴们一般常用于步进电机驱动,其实可以用于电机调速,大功率开关电路,驱动继电器,驱动功率比较大的LED光源,利用PWM来调节亮度哦。

R6、R7、R8电阻:

用于限流,防止ULN2001损坏,导致高压直接输入到MCU的IO。(由于ULN2001D本身自带2.7K电阻,这里的R6、R7、R8可以省略;如果某些驱动芯片没带电阻最好自己加上,具体情况可以查看选用芯片的数据手册作决定)

COM端接电源:

当输出端接感性负载的时候,负载不需要加续流二极管,芯片内部设计有二极管,只需 COM口接负载电源即可,当接其他负载时,COM口可以不接。

在使用阻容降压电路为 ULN2001D 供电时,由于阻容降压电压无法阻止电网上的瞬态高 压波动,必须在 ULN2001D 的 COM 端与地端就近接一个 104 电容,其余应用场合下, 该电容可以不添加。 

五、运算放大器

5.png

利用运放巧妙采集负载的当前电流,可以准确知道当前负载运行情况,有没有正常工作,非常好用。

GND2是负载的地端,通过R16电阻(根据负载电流的大小R16要选功率大一点的)接公共地,会有微小的电压差。

该电路是同相比例运算电路,所以采样端的电压=输入端电压*(1+R9/R11)=69倍的输入电压。大家可以根据测量范围修改R9调节放大倍数。

六、MOS管

控制电源输出通断:

6.png

七、输入电源

7.png

如果电路成本比较紧张,可根据需要适当删减元件。

F1自恢复保险丝:

过流保护,可根据实际负载电流调整阀值大小。

D10 肖基特二极管:

减少后级电源对前级的影响,防止电源正负接反烧坏后级电路,防止电源关电时电流倒灌,但经过二极管有0.4V左右压降,需要考虑经过0.4V降压后会不会低于后级电路的正常工作电压。

TVS管:

输入电压过高保护,一般取正常输入电压的1.4倍。

来源:STM32嵌入式开发

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

围观 112

串行外设接口(SPI)是一种同步串行数据通信接口,常用于 MCU 与外部设备之间进行同步串行通信。CW32L083 内部集成 2 个串行外设 SPI 接口,支持双向全双工、单线半双工和单工通信模式,可配置 MCU 作为 主机或从机,支持多主机通信模式,支持直接内存访问(DMA)。

一、主要功能

• 支持主机模式、从机模式 

• 支持全双工、单线半双工、单工 

• 可选的 4 位到 16 位数据帧宽度 

• 支持收发数据 LSB 或 MSB 在前 

• 可编程时钟极性和时钟相位

• 主机模式下通信速率高达 PCLK/2 

• 从机模式下通信速率高达 PCLK/4 

• 支持多机通信模式 

• 8 个带标志位的中断源 

• 支持直接内存访问 (DMA)

1.功能框图

1.png

SPI 一般通过 4 个引脚与外部设备相连:

• MOSI 主机输出 / 从机输入,用于主机模式下的数据发送和从机模式下的数据接收; 

• MISO 主机输入 / 从机输出,用于主机模式下的数据接收和从机模式下的数据发送; 

• SCK 同步串行时钟,主机时钟输出和从机时钟输入,发送或接收主机同步时钟; 

• CS 从机选择,也用于多机通信时总线冲突检测

CW32L083 支持用户灵活选择 GPIO 作为 SPI 通信引脚,通过 AFR 功能复用实现,具体 SPI 引脚请参考数据手册中的引脚定义。

2.全双工模式

SPI 支持全双工通信模式,在该模式下,主机和从机的移位寄存器通过 MOSI、MISO 两条单向数据线进行连接, 在主机提供的 SCK 时钟信号的控制下同时进行两个方向的数据传输。设置控制寄存器 SPIx_CR1 的 MODE 位域为 0x0,使 SPI 工作于全双工通信模式。

2.png

二、FLASH存储器及电路连接

3.png

W25Q64JVSSIQ为华邦NOR型FLASH,存储容量为8Mbytes,每页256bytes,总共有32768个可编程页,最大一次可编程大小为256bytes。一次擦除大小可以为4K、32K、64K字节(K=1024)或者全擦除。支持标准SPI、Dual SPI、Quad SPI,最大操作时钟为133MHz。

芯片SPI接口时序要求如下:

4.png

由接口时序图可知,空闲电平可高可低,数据在第1个时钟上升沿采样。对应的可以选择用模式0或者模式3和MCU进行对接。

三、实例演示

本实例演示MCU(CW32L083)芯片对外部SPI FLASH(W25Q64JVSSIQ)进行读写访问,SPI接口选择模式0。

1.配置RCC和GPIO

void RCC_Configuration(void)
{
  RCC_HSI_Enable(RCC_HSIOSC_DIV2); //SYSCLK = HSI = 24MHz = HCLK = PCLK
  //外设时钟使能
  RCC_AHBPeriphClk_Enable(DEBUG_UART_GPIO_CLK | RCC_AHB_PERIPH_GPIOC, ENABLE);
  DEBUG_UART_APBClkENx(DEBUG_UART_CLK, ENABLE);
}
             
void GPIO_Configuration(void)//包含UART TX RX 复用和LED开关
{
  GPIO_InitTypeDef GPIO_InitStructure = {0};
  DEBUG_UART_AFTX; //UART TX RX 复用
  DEBUG_UART_AFRX;
  GPIO_InitStructure.Pins = DEBUG_UART_TX_GPIO_PIN;
  GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_Init(DEBUG_UART_TX_GPIO_PORT, &GPIO_InitStructure);
  GPIO_InitStructure.Pins = DEBUG_UART_RX_GPIO_PIN;
  GPIO_InitStructure.Mode = GPIO_MODE_INPUT_PULLUP;
  GPIO_Init(DEBUG_UART_RX_GPIO_PORT, &GPIO_InitStructure);
  GPIO_InitStructure.Pins = GPIO_PIN_3 | GPIO_PIN_2; //PC3 LED1 / PC2 LED2
  GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_Init(CW_GPIOC, &GPIO_InitStructure);
  PC03_SETLOW();//LED灭
  PC02_SETLOW();
}

2.配置UART 

void UART_Configuration(void)
{
   UART_InitTypeDef UART_InitStructure = {0};
   UART_InitStructure.UART_BaudRate = DEBUG_UART_BaudRate; // 波特率
   UART_InitStructure.UART_Over = UART_Over_16; // 采样方式
   UART_InitStructure.UART_Source = UART_Source_PCLK; // 传输时钟源UCLK
   UART_InitStructure.UART_UclkFreq = DEBUG_UART_UclkFreq; // 传输时钟UCLK频率
   UART_InitStructure.UART_StartBit = UART_StartBit_FE; // 起始位判定方式
   UART_InitStructure.UART_StopBits = UART_StopBits_1; // 停止位长度
   UART_InitStructure.UART_Parity = UART_Parity_No ; // 校验方式
   UART_InitStructure.UART_HardwareFlowControl = UART_HardwareFlowControl_None; 
   //硬件流控
   UART_InitStructure.UART_Mode = UART_Mode_Rx | UART_Mode_Tx; // 发送/接收使能
   UART_Init(DEBUG_UARTx, &UART_InitStructure);
}

3.配置SPI接口

void SPI_FLASH_Init(void)
{
  SPI_InitTypeDef  SPI_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;
  /************************GPIO Configuration***********************/
  RCC_AHBPeriphClk_Enable(FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK | FLASH_SPI_MOSI_GPIO_CLK | FLASH_SPI_CS_GPIO_CLK, ENABLE);
  FLASH_SPI_APBClkENx(FLASH_SPI_CLK, ENABLE);
  //SPI SCK MOSI MISO 复用
  FLASH_SPI_AF_SCK;
  FLASH_SPI_AF_MISO;  
  FLASH_SPI_AF_MOSI;     
  //CS
  GPIO_InitStructure.Pins = FLASH_SPI_CS_GPIO_PIN;
  GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStructure.IT = GPIO_IT_NONE;
  GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
  GPIO_Init(FLASH_SPI_CS_GPIO_PORT, &GPIO_InitStructure);
  //SCK   
  GPIO_InitStructure.Pins = FLASH_SPI_SCK_GPIO_PIN;
  GPIO_InitStructure.IT = GPIO_IT_NONE;
  GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);
  //MOSI
  GPIO_InitStructure.Pins = FLASH_SPI_MOSI_GPIO_PIN;
  GPIO_InitStructure.IT = GPIO_IT_NONE;
  GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
  //MISO
  GPIO_InitStructure.Pins = FLASH_SPI_MISO_GPIO_PIN;
  GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
  GPIO_InitStructure.IT = GPIO_IT_NONE;
  GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);
   //拉高CS
  FLASH_SPI_CS_HIGH();
  /************************SPI Configuration***********************/
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;    // 双线全双工
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;              // 主机模式
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;   // 帧数据长度为8bit
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;       // 时钟空闲电平为低
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;     // 第1边沿采样
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;        // 片选信号由SSI寄存器控制
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; 
  // 波特率为PCLK的8分频
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    // 最高有效位 MSB 收发在前
  SPI_InitStructure.SPI_Speed = SPI_Speed_High;    // 低速SPI
  SPI_Init(FLASH_SPIx, &SPI_InitStructure);
}

4.比较两个缓冲区数据

TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
  while(BufferLength--)
  {
     if(*pBuffer1 != *pBuffer2)
     {
       return FAILED;                 
     }
     pBuffer1++;
     pBuffer2++;               
  }
  return PASSED;
}

5.将C库printf函数重新定位到UART

PUTCHAR_PROTOTYPE
{
   UART_SendData_8bit(DEBUG_UARTx, (uint8_t)ch);// 通过UARTx发送一个数据(8bit)
   while (UART_GetFlagStatus(DEBUG_UARTx, UART_FLAG_TXE) == RESET);
   return ch;
}
size_t __write(int handle, const unsigned char * buffer, size_t size)
{
   size_t nChars = 0;     
                 
   if (buffer == 0)
   {//应该刷新内部缓冲区 "handle" == -1
     return 0;
   }
   for (size != 0; --size)
   {
     UART_SendData_8bit(DEBUG_UARTx, *buffer++);
     while (UART_GetFlagStatus(DEBUG_UARTx, UART_FLAG_TXE) == RESET);
     ++nChars;
   }
   return nChars;
}

6.主程序实现SPI对FLASH的读写(具体对FLASH读写可参考cw32_eval_spi_flash.c)

int32_t main(void)
{ 
  RCC_Configuration();//配置RCC
  GPIO_Configuration();//配置GPIO
  UART_Configuration();//配置UART
  SPI_FLASH_Init();//配置SPI
  printf("\r\nCW32L083 SPI FLASH Example\r\n");
  DeviceID = SPI_FLASH_DeviceID();//读取Device ID
  ManufactDeviceID = SPI_FLASH_ManufactDeviceID();//读取Manufacturer / Device ID
  JedecID = SPI_FLASH_JedecID();//读取JEDEC ID
  SPI_FLASH_UniqueID(UniqueID);// 读取Unique ID (64bit)
  printf("\r\nDeviceID = 0x%X\r\nManufactDeviceID = 0x%X\r\nJedecID = 0x%X", DeviceID, ManufactDeviceID, JedecID);
  printf("\r\nUniqueID = 0x ");
  for(uint8_t i = 0; i<8; i++)
  {
     printf("%X ", UniqueID[i]);
  }
  
  if(JedecID == sJedecID)  /* Check JEDEC ID */
  {
     printf("\r\n\nFLASH Detected\r\n");;
     SPI_FLASH_SectorErase(FLASH_SectorToEraseAddress); //擦除扇区 4KB
     SPI_FLASH_BufferWrite(TxBuffer, FLASH_WriteAddress, BufferSize); //写数据
     printf("\r\n写入的数据为:%s\r\n", TxBuffer);
     SPI_FLASH_BufferRead(RxBuffer, FLASH_ReadAddress, BufferSize); //读数据
     printf("\r\n读出的数据为:%s\r\n", RxBuffer);
     //检查写入的数据与读出的数据是否一致
     TransferStatus = Buffercmp(TxBuffer, RxBuffer, BufferSize);
     if(TransferStatus == PASSED)
     {
        PC03_SETHIGH();
        printf("\r\nFLASH Success\r\n");
     }
     else
     {
        PC02_SETHIGH();
        printf("\r\nFLASH Error 1\r\n");
     }
     SPI_FLASH_SectorErase(FLASH_SectorToEraseAddress); //擦除扇区 4KB
     SPI_FLASH_BufferRead(RxBuffer, FLASH_ReadAddress, BufferSize); //读数据
     for(uint8_t j = 0; j < BufferSize; j++)
     {
        if(RxBuffer[j] != 0xFF)
        {
           PC02_SETHIGH();
           printf("\r\nFLASH Error 2\r\n");
        }
     }                
   }
   else// (FlashID != sFLASH_ID)
   {
     PC02_SETHIGH();
     printf("\r\nFLASH Error 3\r\n");
   }
   while(1)
   {
   }
}

7.演示说明

程序读外部FLASH的DeviceID、ManufactDeviceID、JedecID、UniqueID并打印读取到的芯片相关信息,然后依次进行写入数据、读取数据、比较数据、擦除SECTOR、读取数据等操作,当读写正确时,通过UART打印“FLASH Success”,LED1亮。当读取的JedecID不正确,写入读取内容不一致,擦除后读取数据不为全FF时,打印“FLASH Error 2”,LED2亮。如下图所示:

5.png

关于CW32更多详细信息,请访问官方网站www.whxy.com。

来源:武汉芯源半导体

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

围观 234

页面

订阅 RSS - 单片机