闪存编程是指将数据或代码写入单片机的闪存存储器中的过程。在 Microchip 系列的 SAM L 系列(基于 ARM Cortex-M0+)单片机中,闪存编程是一个重要的功能,用于存储应用程序代码、配置数据和用户数据。闪存编程通常涉及以下几个步骤:
擦除闪存:在写入新的数据之前,需要先擦除目标闪存区域。
编程闪存:将新的数据写入闪存。
验证编程:确保写入的数据正确无误。
闪存编程可以通过多种方式进行,包括使用编程器、通过引导加载程序(Bootloader)或者通过应用程序直接编程。本节将详细介绍这些方法及其具体实现。
Microchip 提供了多种编程器,如 MPLAB ICD 3 和 MPLAB IPE(Integrated Programming Environment),这些工具可以帮助开发者轻松地将代码和数据写入 SAM L 系列单片机的闪存。
访问 Microchip 官方网站,下载并安装 MPLAB IPE。
连接编程器到电脑,并确保驱动程序已正确安装。
将编程器通过 USB 线连接到电脑。
将编程器的连接线连接到 SAM L 系列单片机的编程接口(通常是 SWD 或 JTAG 接口)。
打开 MPLAB IPE。
在设备选择菜单中选择目标 SAM L 系列单片机型号。
确认连接并检测目标设备。
在 MPLAB IPE 中选择“擦除”选项。
选择擦除类型(全芯片擦除或部分擦除)。
点击“擦除”按钮,等待操作完成。
在 MPLAB IPE 中选择“编程”选项。
选择要编程的文件(通常是 HEX 文件)。
点击“编程”按钮,等待操作完成。
在 MPLAB IPE 中选择“验证”选项。
选择要验证的文件(通常是 HEX 文件)。
点击“验证”按钮,等待操作完成。
以下是一个使用 MPLAB IPE 进行闪存编程的简单示例:
创建项目:使用 MPLAB X IDE 创建一个新的项目。
编写代码:编写一个简单的 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); // 延时
}
}
生成 HEX 文件:编译项目并生成 HEX 文件。
使用 MPLAB IPE 进行编程:
打开 MPLAB IPE。
选择目标设备。
选择生成的 HEX 文件。
点击“编程”按钮。
引导加载程序(Bootloader)是一种在单片机启动时运行的小程序,用于加载和执行用户应用程序。SAM L 系列单片机支持多种引导加载程序,如通过 UART、SPI 或 USB 进行编程。
初始化 UART:配置 UART 为引导加载程序模式。
设置波特率:选择合适的波特率,如 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) {
// 等待启用完成
}
}
读取 UART 数据:从 UART 接口读取数据。
解析数据:解析接收到的数据,确保其格式正确。
// 读取 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();
}
}
擦除闪存:使用 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) {
// 等待擦除完成
}
}
// 编程闪存
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 为引导加载程序模式。
设置 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
};
读取 USB 数据:从 USB 接口读取数据。
解析数据:解析接收到的数据,确保其格式正确。
// 读取 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();
}
}
擦除闪存:使用 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) {
// 等待擦除完成
}
}
// 编程闪存
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。
接收数据。
解析数据。
擦除并编程闪存。
#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 控制器(Non-Volatile Memory Controller)用于管理和控制闪存的操作,包括擦除、编程和读取。在 SAM L 系列单片机中,NVM 控制器提供了多种命令和寄存器,用于实现闪存编程。通过应用程序直接控制 NVM 控制器可以实现更灵活的闪存管理,例如在运行过程中更新部分代码或数据。
在擦除闪存之前,需要配置 NVM 控制器为手动写模式,然后发送适当的擦除命令。最后,检查 NVM 控制器的状态寄存器,确保擦除操作完成。
配置 NVM 控制器:设置 NVM 控制器为手动写模式。
发送擦除命令:选择合适的擦除命令并发送。
等待擦除完成:检查 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 控制器的状态寄存器,确保写入操作完成。
配置 NVM 控制器:设置 NVM 控制器为手动写模式。
写入数据:将数据写入目标地址。
发送写入命令:发送写入命令并等待操作完成。
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; // 验证成功
}
以下是一个通过应用程序进行闪存编程的简单示例:
配置 NVM 控制器。
擦除闪存。
编程闪存。
验证编程。
#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) {
// 处理错误
}
}
}
擦除和编程操作的顺序:在编程之前必须先擦除目标闪存区域,否则写入操作可能失败。
数据对齐:确保写入的数据对齐到闪存的页大小(通常是 512 字节)。
电源稳定性:在进行闪存编程操作时,确保单片机的电源稳定,避免因电源波动导致编程失败。
错误处理:在实际应用中,应添加适当的错误处理机制,以便在编程或擦除失败时进行恢复或提示。
通过上述方法,开发者可以在不同的场景下灵活地进行闪存编程,确保单片机的可靠性和高效性。无论是使用编程器、引导加载程序还是直接通过应用程序,闪存编程都是嵌入式开发中的一个重要环节。希望本文能帮助开发者更好地理解和掌握闪存编程技术。