Flash from user code (#35)

* support for a subset of the bootrom functions added: fast bit count/ manipulation functions (tested), fast bulk memory fill/ copy functions (tested), flash access functions (NOT tested), debugging support functions (not implemented), miscellaneous functions (not implemented).

* added support for erasing and programming flash from user code. between the first and last call in a programming sequence, the SSI is not in a state where it can handle XIP accesses, so the code that calls the intervening functions must be located in SRAM. this is why I added the time_critical section to rp2040.ld (maybe one should create a dedicated section in ram that is rwx and keep data rwNx).

* flash_program.zig example added
wch-ch32v003
David Sugar 1 year ago committed by GitHub
parent f250134e2f
commit 20e4c9f8f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -56,6 +56,7 @@ pub const Examples = struct {
spi_master: *microzig.EmbeddedExecutable, spi_master: *microzig.EmbeddedExecutable,
uart: *microzig.EmbeddedExecutable, uart: *microzig.EmbeddedExecutable,
//uart_pins: microzig.EmbeddedExecutable, //uart_pins: microzig.EmbeddedExecutable,
flash_program: *microzig.EmbeddedExecutable,
pub fn init(b: *Builder, optimize: std.builtin.OptimizeMode) Examples { pub fn init(b: *Builder, optimize: std.builtin.OptimizeMode) Examples {
var ret: Examples = undefined; var ret: Examples = undefined;

@ -0,0 +1,82 @@
const std = @import("std");
const microzig = @import("microzig");
const rp2040 = microzig.hal;
const flash = rp2040.flash;
const time = rp2040.time;
const gpio = rp2040.gpio;
const clocks = rp2040.clocks;
const led = 25;
const uart_id = 0;
const baud_rate = 115200;
const uart_tx_pin = 0;
const uart_rx_pin = 1;
const flash_target_offset: u32 = 256 * 1024;
const flash_target_contents = @intToPtr([*]const u8, rp2040.flash.XIP_BASE + flash_target_offset);
pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn {
std.log.err("panic: {s}", .{message});
@breakpoint();
while (true) {}
}
pub const std_options = struct {
pub const log_level = .debug;
pub const logFn = rp2040.uart.log;
};
pub fn main() !void {
gpio.reset();
gpio.init(led);
gpio.set_direction(led, .out);
gpio.put(led, 1);
const uart = rp2040.uart.UART.init(uart_id, .{
.baud_rate = baud_rate,
.tx_pin = uart_tx_pin,
.rx_pin = uart_rx_pin,
.clock_config = rp2040.clock_config,
});
rp2040.uart.init_logger(uart);
var data: [flash.PAGE_SIZE]u8 = undefined;
var i: usize = 0;
var j: u8 = 0;
while (i < flash.PAGE_SIZE) : (i += 1) {
data[i] = j;
if (j == 255) j = 0;
j += 1;
}
std.log.info("Generate data", .{});
std.log.info("data: {s}", .{&data});
// Note that a whole number of sectors (4096 bytes) must be erased at a time
std.log.info("Erasing target region...", .{});
flash.range_erase(flash_target_offset, flash.SECTOR_SIZE);
std.log.info("Done. Read back target region:", .{});
std.log.info("data: {s}", .{flash_target_contents[0..flash.PAGE_SIZE]});
// Note that a whole number of pages (256 bytes) must be written at a time
std.log.info("Programming target region...", .{});
flash.range_program(flash_target_offset, &data);
std.log.info("Done. Read back target region:", .{});
std.log.info("data: {s}", .{flash_target_contents[0..flash.PAGE_SIZE]});
var mismatch: bool = false;
i = 0;
while (i < flash.PAGE_SIZE) : (i += 1) {
if (data[i] != flash_target_contents[i])
mismatch = true;
}
if (mismatch) {
std.log.info("Programming failed!", .{});
} else {
std.log.info("Programming successful!", .{});
}
}

@ -10,7 +10,7 @@ ENTRY(microzig_main);
MEMORY MEMORY
{ {
flash0 (rx!w) : ORIGIN = 0x10000000, LENGTH = 0x00200000 flash0 (rx!w) : ORIGIN = 0x10000000, LENGTH = 0x00200000
ram0 (rw!x) : ORIGIN = 0x20000000, LENGTH = 0x00040000 ram0 (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00040000
} }
SECTIONS SECTIONS
@ -43,6 +43,7 @@ SECTIONS
.data : .data :
{ {
microzig_data_start = .; microzig_data_start = .;
*(.time_critical*)
*(.data*) *(.data*)
microzig_data_end = .; microzig_data_end = .;
} > ram0 AT> flash0 } > ram0 AT> flash0

@ -12,6 +12,8 @@ pub const pwm = @import("hal/pwm.zig");
pub const spi = @import("hal/spi.zig"); pub const spi = @import("hal/spi.zig");
pub const resets = @import("hal/resets.zig"); pub const resets = @import("hal/resets.zig");
pub const irq = @import("hal/irq.zig"); pub const irq = @import("hal/irq.zig");
pub const rom = @import("hal/rom.zig");
pub const flash = @import("hal/flash.zig");
pub const clock_config = clocks.GlobalConfiguration.init(.{ pub const clock_config = clocks.GlobalConfiguration.init(.{
.ref = .{ .source = .src_xosc }, .ref = .{ .source = .src_xosc },

@ -0,0 +1,81 @@
const rom = @import("rom.zig");
pub const Command = enum(u8) {
block_erase = 0xd8,
ruid_cmd = 0x4b,
};
pub const PAGE_SIZE = 256;
pub const SECTOR_SIZE = 4096;
pub const BLOCK_SIZE = 65536;
/// Bus reads to a 16MB memory window start at this address
pub const XIP_BASE = 0x10000000;
pub const boot2 = struct {
/// Size of the second stage bootloader in bytes
const BOOT2_SIZE_BYTES = 64;
/// Buffer for the second stage bootloader
var copyout: [BOOT2_SIZE_BYTES]u32 = undefined;
var copyout_valid: bool = false;
/// Copy the 2nd stage bootloader into memory
pub fn flash_init() linksection(".time_critical") void {
if (copyout_valid) return;
const bootloader = @intToPtr([*]u32, XIP_BASE);
var i: usize = 0;
while (i < BOOT2_SIZE_BYTES) : (i += 1) {
copyout[i] = bootloader[i];
}
copyout_valid = true;
}
pub fn flash_enable_xip() linksection(".time_critical") void {
// TODO: use the second stage bootloader instead of cmd_xip
//const bootloader: []u32 = copyout[1..];
//const f = @ptrCast(*fn () void, bootloader.ptr);
//f();
rom.flash_enter_cmd_xip()();
}
};
/// Erase count bytes starting at offset (offset from start of flash)
///
/// The offset must be aligned to a 4096-byte sector, and count must
/// be a multiple of 4096 bytes!
pub fn range_erase(offset: u32, count: u32) linksection(".time_critical") void {
// TODO: add sanity checks, e.g., offset + count < flash size
boot2.flash_init();
// TODO: __compiler_memory_barrier
rom.connect_internal_flash()();
rom.flash_exit_xip()();
rom.flash_range_erase()(offset, count, BLOCK_SIZE, @enumToInt(Command.block_erase));
rom.flash_flush_cache()();
boot2.flash_enable_xip();
}
/// Program data to flash starting at offset (offset from the start of flash)
///
/// The offset must be aligned to a 256-byte boundary, and the length of data
/// must be a multiple of 256!
pub fn range_program(offset: u32, data: []const u8) linksection(".time_critical") void {
// TODO: add sanity checks, e.g., offset + count < flash size
boot2.flash_init();
// TODO: __compiler_memory_barrier
rom.connect_internal_flash()();
rom.flash_exit_xip()();
rom.flash_range_program()(offset, data.ptr, data.len);
rom.flash_flush_cache()();
boot2.flash_enable_xip();
}

@ -0,0 +1,269 @@
//! Access to functions and data in the RP2040 bootrom
//!
//! The Bootrom contains a number of public functions that provide useful RP2040 functionality that might be needed in
//! the absence of any other code on the device, as well as highly optimized versions of certain key functionality that would
//! otherwise have to take up space in most user binaries.
//!
//! The functions include:
//! 1. Fast Bit Counting / Manipulation Functions
//! 2. Fast Bulk Memory Fill / Copy Functions
//! 3. Flash Access Functions
//! 4. Debugging Support Functions (TODO)
//! 5. Miscellaneous Functions (TODO)
/// Function codes to lookup public functions that provide useful RP2040 functionality
pub const Code = enum(u32) {
popcount32 = rom_table_code('P', '3'),
reverse32 = rom_table_code('R', '3'),
clz32 = rom_table_code('L', '3'),
ctz32 = rom_table_code('T', '3'),
memset = rom_table_code('M', 'S'),
memset4 = rom_table_code('S', '4'),
memcpy = rom_table_code('M', 'C'),
memcpy44 = rom_table_code('C', '4'),
reset_usb_boot = rom_table_code('U', 'B'),
connect_internal_flash = rom_table_code('I', 'F'),
flash_exit_xip = rom_table_code('E', 'X'),
flash_range_erase = rom_table_code('R', 'E'),
flash_range_program = rom_table_code('R', 'P'),
flash_flush_cache = rom_table_code('F', 'C'),
flash_enter_cmd_xip = rom_table_code('C', 'X'),
};
/// Signatures of all public bootrom functions
pub const signatures = struct {
/// Returns the 32 bit pointer into the ROM if found or NULL otherwise
const rom_table_lookup = fn (table: *u16, code: u32) *anyopaque;
/// Signature for popcount32: Return a count of the number of 1 bits in value
const popcount32 = fn (value: u32) u32;
/// Signature for reverse32: Return the bits of value in the reverse order
const reverse32 = fn (value: u32) u32;
/// Signature for clz32: Return the number of consecutive high order 0 bits of value
const clz32 = fn (value: u32) u32;
/// Signature for ctz32: Return the number of consecutive low order 0 bits of value
const ctz32 = fn (value: u32) u32;
/// Signature of memset: Sets n bytes start at ptr to the value c and returns ptr
const memset = fn (ptr: [*]u8, c: u8, n: u32) [*]u8;
/// Signature of memset4: Sets n bytes start at ptr to the value c and returns ptr; must be word (32-bit) aligned!
const memset4 = fn (ptr: [*]u32, c: u8, n: u32) [*]u32;
/// Signature of memcpy: Copies n bytes starting at src to dest and returns dest. The results are undefined if the regions overlap.
const memcpy = fn (dest: [*]u8, src: [*]u8, n: u32) [*]u8;
/// Signature of memcpy44: Copies n bytes starting at src to dest and returns dest; must be word (32-bit) aligned!
const memcpy44 = fn (dest: [*]u32, src: [*]u32, n: u32) [*]u8;
/// Signature of connect_internal_flash: Restore all QSPI pad controls to their default state, and connect the SSI to the QSPI pads
const connect_internal_flash = fn () void;
/// Signature of flash_exit_xip: First set up the SSI for serial-mode operations, then issue the fixed XIP exit sequence described in
/// Section 2.8.1.2. Note that the bootrom code uses the IO forcing logic to drive the CS pin, which must be
/// cleared before returning the SSI to XIP mode (e.g. by a call to _flash_flush_cache). This function
/// configures the SSI with a fixed SCK clock divisor of /6.
const flash_exit_xip = fn () void;
/// Signature of flash_range_erase: Erase a count bytes, starting at addr (offset from start of flash). Optionally, pass a block erase command
/// e.g. D8h block erase, and the size of the block erased by this command this function will use the larger
/// block erase where possible, for much higher erase speed. addr must be aligned to a 4096-byte sector, and
/// count must be a multiple of 4096 bytes.
const flash_range_erase = fn (addr: u32, count: usize, block_size: u32, block_cmd: u8) void;
/// Signature of flash_range_program: Program data to a range of flash addresses starting at addr (offset from the start of flash) and count bytes
/// in size. addr must be aligned to a 256-byte boundary, and count must be a multiple of 256.
const flash_range_program = fn (addr: u32, data: [*]const u8, count: usize) void;
/// Signature of flash_flush_cache: Flush and enable the XIP cache. Also clears the IO forcing on QSPI CSn, so that the SSI can drive the
/// flash chip select as normal.
const flash_flush_cache = fn () void;
/// Signature of flash_enter_cmd_xip: Configure the SSI to generate a standard 03h serial read command, with 24 address bits, upon each XIP
/// access. This is a very slow XIP configuration, but is very widely supported. The debugger calls this
/// function after performing a flash erase/programming operation, so that the freshly-programmed code
/// and data is visible to the debug host, without having to know exactly what kind of flash device is
/// connected.
const flash_enter_cmd_xip = fn () void;
};
/// Return a bootrom lookup code based on two ASCII characters
///
/// These codes are uses to lookup data or function addresses in the bootrom
///
/// # Parameters
/// * `c1` - the first character
/// * `c2` - the second character
///
/// # Returns
///
/// A 32 bit address pointing into bootrom
pub fn rom_table_code(c1: u8, c2: u8) u32 {
return @intCast(u32, c1) | (@intCast(u32, c2) << 8);
}
/// Convert a 16 bit pointer stored at the given rom address into a pointer
///
/// # Parameters
/// * `rom_addr` - address of the 16-bit pointer in rom
///
/// # Returns
///
/// The converted pointer
pub inline fn rom_hword_as_ptr(rom_addr: u32) *anyopaque {
const ptr_to_ptr = @intToPtr(*u16, rom_addr);
return @intToPtr(*anyopaque, @intCast(usize, ptr_to_ptr.*));
}
/// Lookup a bootrom function by code (inline)
///
/// # Parameters
/// * `code` - code of the function (see codes)
///
/// # Returns
///
/// A anyopaque pointer to the function; must be cast by the caller
pub inline fn _rom_func_lookup(code: Code) *anyopaque {
const rom_table_lookup = @ptrCast(*signatures.rom_table_lookup, rom_hword_as_ptr(0x18));
const func_table = @ptrCast(*u16, @alignCast(2, rom_hword_as_ptr(0x14)));
return rom_table_lookup(func_table, @enumToInt(code));
}
/// Lookup a bootrom function by code
///
/// # Parameters
/// * `code` - code of the function (see codes)
///
/// # Returns
///
/// A anyopaque pointer to the function; must be cast by the caller
pub fn rom_func_lookup(code: Code) *anyopaque {
return _rom_func_lookup(code);
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Fast Bit Counting / Manipulation Functions (Datasheet p. 135)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/// Return a count of the number of 1 bits in value
pub fn popcount32(value: u32) u32 {
const S = struct {
var f: ?*signatures.popcount32 = null;
};
if (S.f == null) S.f = @ptrCast(*signatures.popcount32, _rom_func_lookup(Code.popcount32));
return S.f.?(value);
}
/// Return a count of the number of 1 bits in value
pub fn reverse32(value: u32) u32 {
const S = struct {
var f: ?*signatures.reverse32 = null;
};
if (S.f == null) S.f = @ptrCast(*signatures.reverse32, _rom_func_lookup(Code.reverse32));
return S.f.?(value);
}
/// Return the number of consecutive high order 0 bits of value
pub fn clz32(value: u32) u32 {
const S = struct {
var f: ?*signatures.clz32 = null;
};
if (S.f == null) S.f = @ptrCast(*signatures.clz32, _rom_func_lookup(Code.clz32));
return S.f.?(value);
}
/// Return the number of consecutive low order 0 bits of value
pub fn ctz32(value: u32) u32 {
const S = struct {
var f: ?*signatures.ctz32 = null;
};
if (S.f == null) S.f = @ptrCast(*signatures.ctz32, _rom_func_lookup(Code.ctz32));
return S.f.?(value);
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Fast Bulk Memory Fill / Copy Functions (Datasheet p. 136)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/// Sets all bytes of dest to the value c and returns ptr
pub fn memset(dest: []u8, c: u8) []u8 {
const S = struct {
var f: ?*signatures.memset = null;
};
if (S.f == null) S.f = @ptrCast(*signatures.memset, _rom_func_lookup(Code.memset));
return S.f.?(dest.ptr, c, dest.len)[0..dest.len];
}
/// Copies n bytes from src to dest; The number of bytes copied is the size of the smaller slice
pub fn memcpy(dest: []u8, src: []u8) []u8 {
const S = struct {
var f: ?*signatures.memcpy = null;
};
const n = if (dest.len <= src.len) dest.len else src.len;
if (S.f == null) S.f = @ptrCast(*signatures.memcpy, _rom_func_lookup(Code.memcpy));
return S.f.?(dest.ptr, src.ptr, n)[0..n];
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Flash Access Functions (Datasheet p. 137)
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/// Restore all QSPI pad controls to their default state, and connect the SSI to the QSPI pads
pub inline fn connect_internal_flash() *signatures.connect_internal_flash {
return @ptrCast(
*signatures.connect_internal_flash,
_rom_func_lookup(Code.connect_internal_flash),
);
}
/// First set up the SSI for serial-mode operations, then issue the fixed XIP exit
/// sequence described in Section 2.8.1.2. Note that the bootrom code uses the IO
/// forcing logic to drive the CS pin, which must be cleared before returning the
/// SSI to XIP mode (e.g. by a call to _flash_flush_cache). This function configures
/// the SSI with a fixed SCK clock divisor of /6.
pub inline fn flash_exit_xip() *signatures.flash_exit_xip {
return @ptrCast(
*signatures.flash_exit_xip,
_rom_func_lookup(Code.flash_exit_xip),
);
}
/// Erase a count bytes, starting at addr (offset from start of flash). Optionally,
/// pass a block erase command e.g. D8h block erase, and the size of the block
/// erased by this command this function will use the larger block erase where
/// possible, for much higher erase speed. addr must be aligned to a 4096-byte sector,
/// and count must be a multiple of 4096 bytes.
pub inline fn flash_range_erase() *signatures.flash_range_erase {
return @ptrCast(
*signatures.flash_range_erase,
_rom_func_lookup(Code.flash_range_erase),
);
}
/// Program data to a range of flash addresses starting at addr (offset from the
/// start of flash) and count bytes in size. addr must be aligned to a 256-byte
/// boundary, and the length of data must be a multiple of 256.
pub inline fn flash_range_program() *signatures.flash_range_program {
return @ptrCast(
*signatures.flash_range_program,
_rom_func_lookup(Code.flash_range_program),
);
}
/// Flush and enable the XIP cache. Also clears the IO forcing on QSPI CSn, so that
/// the SSI can drive the flash chip select as normal.
pub inline fn flash_flush_cache() *signatures.flash_flush_cache {
return @ptrCast(
*signatures.flash_flush_cache,
_rom_func_lookup(Code.flash_flush_cache),
);
}
/// Configure the SSI to generate a standard 03h serial read command, with 24 address
/// bits, upon each XIP access. This is a very slow XIP configuration, but is very
/// widely supported. The debugger calls this function after performing a flash
/// erase/programming operation, so that the freshly-programmed code and data is
/// visible to the debug host, without having to know exactly what kind of flash
/// device is connected.
pub inline fn flash_enter_cmd_xip() *signatures.flash_enter_cmd_xip {
return @ptrCast(
*signatures.flash_enter_cmd_xip,
_rom_func_lookup(Code.flash_enter_cmd_xip),
);
}
Loading…
Cancel
Save