Microchip 系列:SAM L 系列 (基于 ARM Cortex-M0+)_(15).闪存编程技术

闪存编程技术

在这里插入图片描述

闪存编程概述

闪存编程是指将数据或代码写入单片机的闪存存储器中的过程。在 Microchip 系列的 SAM L 系列(基于 ARM Cortex-M0+)单片机中,闪存编程是一个重要的功能,用于存储应用程序代码、配置数据和用户数据。闪存编程通常涉及以下几个步骤:

  1. 擦除闪存:在写入新的数据之前,需要先擦除目标闪存区域。

  2. 编程闪存:将新的数据写入闪存。

  3. 验证编程:确保写入的数据正确无误。

闪存编程可以通过多种方式进行,包括使用编程器、通过引导加载程序(Bootloader)或者通过应用程序直接编程。本节将详细介绍这些方法及其具体实现。

使用编程器进行闪存编程

编程器类型

Microchip 提供了多种编程器,如 MPLAB ICD 3 和 MPLAB IPE(Integrated Programming Environment),这些工具可以帮助开发者轻松地将代码和数据写入 SAM L 系列单片机的闪存。

使用 MPLAB IPE 进行闪存编程

安装 MPLAB IPE
  1. 访问 Microchip 官方网站,下载并安装 MPLAB IPE。

  2. 连接编程器到电脑,并确保驱动程序已正确安装。

连接编程器
  1. 将编程器通过 USB 线连接到电脑。

  2. 将编程器的连接线连接到 SAM L 系列单片机的编程接口(通常是 SWD 或 JTAG 接口)。

选择目标设备
  1. 打开 MPLAB IPE。

  2. 在设备选择菜单中选择目标 SAM L 系列单片机型号。

  3. 确认连接并检测目标设备。

擦除闪存
  1. 在 MPLAB IPE 中选择“擦除”选项。

  2. 选择擦除类型(全芯片擦除或部分擦除)。

  3. 点击“擦除”按钮,等待操作完成。

编程闪存
  1. 在 MPLAB IPE 中选择“编程”选项。

  2. 选择要编程的文件(通常是 HEX 文件)。

  3. 点击“编程”按钮,等待操作完成。

验证编程
  1. 在 MPLAB IPE 中选择“验证”选项。

  2. 选择要验证的文件(通常是 HEX 文件)。

  3. 点击“验证”按钮,等待操作完成。

示例代码

以下是一个使用 MPLAB IPE 进行闪存编程的简单示例:

  1. 创建项目:使用 MPLAB X IDE 创建一个新的项目。

  2. 编写代码:编写一个简单的 LED 闪烁程序。


// LED 闪烁程序

#include "sam.h"



#define LED_PIN (1 << 17) // PA17 为 LED 引脚



void delay(uint32_t count) {

    while (count--) {

        __NOP(); // 无操作指令

    }

}



int main(void) {

    // 初始化 GPIO

    PORT->Group[0].DIRSET = LED_PIN; // 设置 PA17 为输出



    while (1) {

        PORT->Group[0].OUTSET = LED_PIN; // 点亮 LED

        delay(1000000); // 延时

        PORT->Group[0].OUTCLR = LED_PIN; // 熄灭 LED

        delay(1000000); // 延时

    }

}

  1. 生成 HEX 文件:编译项目并生成 HEX 文件。

  2. 使用 MPLAB IPE 进行编程

    • 打开 MPLAB IPE。

    • 选择目标设备。

    • 选择生成的 HEX 文件。

    • 点击“编程”按钮。

通过引导加载程序进行闪存编程

引导加载程序概述

引导加载程序(Bootloader)是一种在单片机启动时运行的小程序,用于加载和执行用户应用程序。SAM L 系列单片机支持多种引导加载程序,如通过 UART、SPI 或 USB 进行编程。

通过 UART 进行闪存编程

配置 UART
  1. 初始化 UART:配置 UART 为引导加载程序模式。

  2. 设置波特率:选择合适的波特率,如 115200 bps。


// UART 初始化

void UART_Init(void) {

    // 配置 UART 时钟

    GCLK->GCLK_CLKCTRL.reg = GCLK_CLKCTRL_ID(UART0_GCLK_ID) | GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0;



    // 配置 UART 控制寄存器

    UART0->UART_CTRLA.reg = UART_CTRLA_SWRST; // 复位 UART

    while (UART0->UART_SYNCBUSY.bit.CTRLA) {

        // 等待复位完成

    }



    UART0->UART_CTRLA.reg = UART_CTRLA_MODE_USART_INT_CLK | UART_CTRLA_RXPO(1) | UART_CTRLA_TXPO(0);

    UART0->UART_BAUD.reg = UART_BAUD_BAUD(0x1A); // 设置波特率为 115200 bps

    UART0->UART_CTRLB.reg = UART_CTRLB_TXEN | UART_CTRLB_RXEN; // 启用发送和接收



    // 启用 UART

    UART0->UART_CTRLA.reg |= UART_CTRLA_ENABLE;

    while (UART0->UART_SYNCBUSY.bit.ENABLE) {

        // 等待启用完成

    }

}

接收数据
  1. 读取 UART 数据:从 UART 接口读取数据。

  2. 解析数据:解析接收到的数据,确保其格式正确。


// 读取 UART 数据

uint8_t UART_ReadByte(void) {

    while (!UART0->UART_STATUS.bit.RXRDY) {

        // 等待数据准备好

    }

    return UART0->UART_DATA.reg;

}



// 解析数据

void UART_ParseData(uint8_t *data, uint32_t length) {

    // 解析数据并存储到缓冲区

    for (uint32_t i = 0; i < length; i++) {

        data[i] = UART_ReadByte();

    }

}

写入闪存
  1. 擦除闪存:使用 NVM 控制器擦除目标闪存区域。

  2. 编程闪存:将解析后的数据写入闪存。


// 擦除闪存

void Flash_Erase(uint32_t address) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 擦除命令

    NVMCTRL->ADDR.reg = address; // 目标地址

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 再次发送擦除命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待擦除完成

    }

}



// 编程闪存

void Flash_Write(uint32_t address, uint8_t *data, uint32_t length) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 写入命令

    NVMCTRL->ADDR.reg = address; // 目标地址



    // 将数据写入闪存

    for (uint32_t i = 0; i < length; i++) {

        ((uint8_t *)address)[i] = data[i];

    }



    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 再次发送写入命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待写入完成

    }

}

通过 USB 进行闪存编程

配置 USB
  1. 初始化 USB:配置 USB 为引导加载程序模式。

  2. 设置 USB 描述符:定义 USB 描述符,使其符合 USB 规范。


// USB 初始化

void USB_Init(void) {

    // 配置 USB 时钟

    GCLK->GCLK_CLKCTRL.reg = GCLK_CLKCTRL_ID(USB_GCLK_ID) | GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0;



    // 配置 USB 控制寄存器

    USB->DEVICE.CTRLA.reg = USB_CTRLA_SWRST; // 复位 USB

    while (USB->DEVICE.SYNCBUSY.bit.CTRLA) {

        // 等待复位完成

    }



    USB->DEVICE.CTRLA.reg = USB_CTRLA_ENABLE; // 启用 USB

    while (USB->DEVICE.SYNCBUSY.bit.ENABLE) {

        // 等待启用完成

    }



    // 设置 USB 描述符

    USB->DEVICE.DESCABORT.reg = 0;

    USB->DEVICE.DADD.reg = 0;

    USB->DEVICE.CTRLB.reg = USB_CTRLB_DETACH;

}



// USB 描述符

const uint8_t USB_Descriptor[] = {

    // 设备描述符

    0x12, 0x01, 0x10, 0x01, 0x00, 0x02, 0x02, 0x40, 0x00, 0x01, 0x00, 0x01, 0x08, 0x06, 0x00, 0x01, 0x00, 0x01,

    // 配置描述符

    0x09, 0x02, 0x20, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,

    // 接口描述符

    0x09, 0x04, 0x00, 0x00, 0x02, 0x0A, 0x00, 0x00, 0x07,

    // 端点描述符

    0x07, 0x05, 0x01, 0x02, 0x02, 0x04, 0x00,

    0x07, 0x05, 0x81, 0x02, 0x02, 0x04, 0x00

};

接收数据
  1. 读取 USB 数据:从 USB 接口读取数据。

  2. 解析数据:解析接收到的数据,确保其格式正确。


// 读取 USB 数据

uint8_t USB_ReadByte(void) {

    while (!USB->DEVICE.INTFLAG.bit.ENDRX) {

        // 等待数据准备好

    }

    return USB->DEVICE.INTFLAG.bit.ENDRX;

}



// 解析数据

void USB_ParseData(uint8_t *data, uint32_t length) {

    // 解析数据并存储到缓冲区

    for (uint32_t i = 0; i < length; i++) {

        data[i] = USB_ReadByte();

    }

}

写入闪存
  1. 擦除闪存:使用 NVM 控制器擦除目标闪存区域。

  2. 编程闪存:将解析后的数据写入闪存。


// 擦除闪存

void Flash_Erase(uint32_t address) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 擦除命令

    NVMCTRL->ADDR.reg = address; // 目标地址

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 再次发送擦除命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待擦除完成

    }

}



// 编程闪存

void Flash_Write(uint32_t address, uint8_t *data, uint32_t length) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 写入命令

    NVMCTRL->ADDR.reg = address; // 目标地址



    // 将数据写入闪存

    for (uint32_t i = 0; i < length; i++) {

        ((uint8_t *)address)[i] = data[i];

    }



    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 再次发送写入命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待写入完成

    }

}

示例代码

以下是一个通过 USB 进行闪存编程的简单示例:

  1. 初始化 USB

  2. 接收数据

  3. 解析数据

  4. 擦除并编程闪存


#include "sam.h"



#define FLASH_PAGE_SIZE 512



uint8_t buffer[FLASH_PAGE_SIZE]; // 缓冲区



void USB_Init(void) {

    // 配置 USB 时钟

    GCLK->GCLK_CLKCTRL.reg = GCLK_CLKCTRL_ID(USB_GCLK_ID) | GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0;



    // 配置 USB 控制寄存器

    USB->DEVICE.CTRLA.reg = USB_CTRLA_SWRST; // 复位 USB

    while (USB->DEVICE.SYNCBUSY.bit.CTRLA) {

        // 等待复位完成

    }



    USB->DEVICE.CTRLA.reg = USB_CTRLA_ENABLE; // 启用 USB

    while (USB->DEVICE.SYNCBUSY.bit.ENABLE) {

        // 等待启用完成

    }



    // 设置 USB 描述符

    USB->DEVICE.DESCABORT.reg = 0;

    USB->DEVICE.DADD.reg = 0;

    USB->DEVICE.CTRLB.reg = USB_CTRLB_DETACH;

}



uint8_t USB_ReadByte(void) {

    while (!USB->DEVICE.INTFLAG.bit.ENDRX) {

        // 等待数据准备好

    }

    return USB->DEVICE.INTFLAG.bit.ENDRX;

}



void USB_ParseData(uint8_t *data, uint32_t length) {

    // 解析数据并存储到缓冲区

    for (uint32_t i = 0; i < length; i++) {

        data[i] = USB_ReadByte();

    }

}



void Flash_Erase(uint32_t address) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 擦除命令

    NVMCTRL->ADDR.reg = address; // 目标地址

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 再次发送擦除命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待擦除完成

    }

}



void Flash_Write(uint32_t address, uint8_t *data, uint32_t length) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 写入命令

    NVMCTRL->ADDR.reg = address; // 目标地址



    // 将数据写入闪存

    for (uint32_t i = 0; i < length; i++) {

        ((uint8_t *)address)[i] = data[i];

    }



    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 再次发送写入命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待写入完成

    }

}



void main(void) {

    USB_Init(); // 初始化 USB



    uint32_t address = 0x00400000; // 目标闪存地址

    uint32_t length = FLASH_PAGE_SIZE; // 数据长度



    USB_ParseData(buffer, length); // 从 USB 接口接收数据

    Flash_Erase(address); // 擦除目标闪存区域

    Flash_Write(address, buffer, length); // 将数据写入闪存



    while (1) {

        // 等待编程完成

    }

}

通过应用程序进行闪存编程

NVM 控制器概述

NVM 控制器(Non-Volatile Memory Controller)用于管理和控制闪存的操作,包括擦除、编程和读取。在 SAM L 系列单片机中,NVM 控制器提供了多种命令和寄存器,用于实现闪存编程。通过应用程序直接控制 NVM 控制器可以实现更灵活的闪存管理,例如在运行过程中更新部分代码或数据。

擦除闪存

在擦除闪存之前,需要配置 NVM 控制器为手动写模式,然后发送适当的擦除命令。最后,检查 NVM 控制器的状态寄存器,确保擦除操作完成。

  1. 配置 NVM 控制器:设置 NVM 控制器为手动写模式。

  2. 发送擦除命令:选择合适的擦除命令并发送。

  3. 等待擦除完成:检查 NVM 控制器的状态寄存器,确保擦除操作完成。


void Flash_Erase(uint32_t address) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 擦除命令

    NVMCTRL->ADDR.reg = address; // 目标地址

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 再次发送擦除命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待擦除完成

    }

}

编程闪存

在编程闪存之前,同样需要配置 NVM 控制器为手动写模式,然后将数据写入目标地址,并发送写入命令。最后,检查 NVM 控制器的状态寄存器,确保写入操作完成。

  1. 配置 NVM 控制器:设置 NVM 控制器为手动写模式。

  2. 写入数据:将数据写入目标地址。

  3. 发送写入命令:发送写入命令并等待操作完成。


void Flash_Write(uint32_t address, uint8_t *data, uint32_t length) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 写入命令

    NVMCTRL->ADDR.reg = address; // 目标地址



    // 将数据写入闪存

    for (uint32_t i = 0; i < length; i++) {

        ((uint8_t *)address)[i] = data[i];

    }



    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 再次发送写入命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待写入完成

    }

}

验证编程

验证编程是为了确保写入的数据正确无误。可以通过读取目标地址的数据并与原始数据进行比较来实现。

  1. 读取数据:从闪存目标地址读取数据。

  2. 比较数据:将读取的数据与原始数据进行比较,确保一致。


int Flash_Verify(uint32_t address, uint8_t *data, uint32_t length) {

    // 读取闪存数据

    for (uint32_t i = 0; i < length; i++) {

        if (((uint8_t *)address)[i] != data[i]) {

            return -1; // 数据不一致

        }

    }

    return 0; // 验证成功

}

示例代码

以下是一个通过应用程序进行闪存编程的简单示例:

  1. 配置 NVM 控制器

  2. 擦除闪存

  3. 编程闪存

  4. 验证编程


#include "sam.h"



#define FLASH_PAGE_SIZE 512

#define FLASH_START_ADDRESS 0x00400000



uint8_t buffer[FLASH_PAGE_SIZE]; // 缓冲区



// 擦除闪存

void Flash_Erase(uint32_t address) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 擦除命令

    NVMCTRL->ADDR.reg = address; // 目标地址

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_ER; // 再次发送擦除命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待擦除完成

    }

}



// 编程闪存

void Flash_Write(uint32_t address, uint8_t *data, uint32_t length) {

    // 配置 NVM 控制器

    NVMCTRL->CTRLB.bit.MANW = 1; // 手动写模式

    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 写入命令

    NVMCTRL->ADDR.reg = address; // 目标地址



    // 将数据写入闪存

    for (uint32_t i = 0; i < length; i++) {

        ((uint8_t *)address)[i] = data[i];

    }



    NVMCTRL->CTRLA.bit.CMD = NVMCTRL_CMDA_WP; // 再次发送写入命令

    while (NVMCTRL->INTFLAG.bit.READY == 0) {

        // 等待写入完成

    }

}



// 验证编程

int Flash_Verify(uint32_t address, uint8_t *data, uint32_t length) {

    // 读取闪存数据

    for (uint32_t i = 0; i < length; i++) {

        if (((uint8_t *)address)[i] != data[i]) {

            return -1; // 数据不一致

        }

    }

    return 0; // 验证成功

}



void main(void) {

    // 初始化数据

    for (uint32_t i = 0; i < FLASH_PAGE_SIZE; i++) {

        buffer[i] = i % 256; // 填充缓冲区

    }



    uint32_t address = FLASH_START_ADDRESS; // 目标闪存地址

    uint32_t length = FLASH_PAGE_SIZE; // 数据长度



    // 擦除目标闪存区域

    Flash_Erase(address);



    // 将数据写入闪存

    Flash_Write(address, buffer, length);



    // 验证编程

    if (Flash_Verify(address, buffer, length) == 0) {

        // 验证成功

        while (1) {

            // 等待

        }

    } else {

        // 验证失败

        while (1) {

            // 处理错误

        }

    }

}

注意事项

  1. 擦除和编程操作的顺序:在编程之前必须先擦除目标闪存区域,否则写入操作可能失败。

  2. 数据对齐:确保写入的数据对齐到闪存的页大小(通常是 512 字节)。

  3. 电源稳定性:在进行闪存编程操作时,确保单片机的电源稳定,避免因电源波动导致编程失败。

  4. 错误处理:在实际应用中,应添加适当的错误处理机制,以便在编程或擦除失败时进行恢复或提示。

通过上述方法,开发者可以在不同的场景下灵活地进行闪存编程,确保单片机的可靠性和高效性。无论是使用编程器、引导加载程序还是直接通过应用程序,闪存编程都是嵌入式开发中的一个重要环节。希望本文能帮助开发者更好地理解和掌握闪存编程技术。

你可能感兴趣的:(单片机开发,arm开发,mongodb,数据库,嵌入式硬件,单片机,物联网)