mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-05 03:27:48 +00:00
6e1b8a54aa
machine/stm32, nrf: implement machine.Flash Implements the machine.Flash interface using the same definition as the tinyfs BlockDevice. This implementation covers the stm32f4, stm32l4, stm32wlx, nrf51, nrf52, and nrf528xx processors.
493 lines
15 KiB
Go
493 lines
15 KiB
Go
//go:build nrf
|
|
|
|
package machine
|
|
|
|
import (
|
|
"bytes"
|
|
"device/nrf"
|
|
"encoding/binary"
|
|
"runtime/interrupt"
|
|
"unsafe"
|
|
)
|
|
|
|
const deviceName = nrf.Device
|
|
|
|
const (
|
|
PinInput PinMode = (nrf.GPIO_PIN_CNF_DIR_Input << nrf.GPIO_PIN_CNF_DIR_Pos) | (nrf.GPIO_PIN_CNF_INPUT_Connect << nrf.GPIO_PIN_CNF_INPUT_Pos)
|
|
PinInputPullup PinMode = PinInput | (nrf.GPIO_PIN_CNF_PULL_Pullup << nrf.GPIO_PIN_CNF_PULL_Pos)
|
|
PinInputPulldown PinMode = PinInput | (nrf.GPIO_PIN_CNF_PULL_Pulldown << nrf.GPIO_PIN_CNF_PULL_Pos)
|
|
PinOutput PinMode = (nrf.GPIO_PIN_CNF_DIR_Output << nrf.GPIO_PIN_CNF_DIR_Pos) | (nrf.GPIO_PIN_CNF_INPUT_Connect << nrf.GPIO_PIN_CNF_INPUT_Pos)
|
|
)
|
|
|
|
type PinChange uint8
|
|
|
|
// Pin change interrupt constants for SetInterrupt.
|
|
const (
|
|
PinRising PinChange = nrf.GPIOTE_CONFIG_POLARITY_LoToHi
|
|
PinFalling PinChange = nrf.GPIOTE_CONFIG_POLARITY_HiToLo
|
|
PinToggle PinChange = nrf.GPIOTE_CONFIG_POLARITY_Toggle
|
|
)
|
|
|
|
// Callbacks to be called for pins configured with SetInterrupt.
|
|
var pinCallbacks [len(nrf.GPIOTE.CONFIG)]func(Pin)
|
|
|
|
// Configure this pin with the given configuration.
|
|
func (p Pin) Configure(config PinConfig) {
|
|
cfg := config.Mode | nrf.GPIO_PIN_CNF_DRIVE_S0S1 | nrf.GPIO_PIN_CNF_SENSE_Disabled
|
|
port, pin := p.getPortPin()
|
|
port.PIN_CNF[pin].Set(uint32(cfg))
|
|
}
|
|
|
|
// Set the pin to high or low.
|
|
// Warning: only use this on an output pin!
|
|
func (p Pin) Set(high bool) {
|
|
port, pin := p.getPortPin()
|
|
if high {
|
|
port.OUTSET.Set(1 << pin)
|
|
} else {
|
|
port.OUTCLR.Set(1 << pin)
|
|
}
|
|
}
|
|
|
|
// Return the register and mask to enable a given GPIO pin. This can be used to
|
|
// implement bit-banged drivers.
|
|
func (p Pin) PortMaskSet() (*uint32, uint32) {
|
|
port, pin := p.getPortPin()
|
|
return &port.OUTSET.Reg, 1 << pin
|
|
}
|
|
|
|
// Return the register and mask to disable a given port. This can be used to
|
|
// implement bit-banged drivers.
|
|
func (p Pin) PortMaskClear() (*uint32, uint32) {
|
|
port, pin := p.getPortPin()
|
|
return &port.OUTCLR.Reg, 1 << pin
|
|
}
|
|
|
|
// Get returns the current value of a GPIO pin when the pin is configured as an
|
|
// input or as an output.
|
|
func (p Pin) Get() bool {
|
|
port, pin := p.getPortPin()
|
|
return (port.IN.Get()>>pin)&1 != 0
|
|
}
|
|
|
|
// SetInterrupt sets an interrupt to be executed when a particular pin changes
|
|
// state. The pin should already be configured as an input, including a pull up
|
|
// or down if no external pull is provided.
|
|
//
|
|
// This call will replace a previously set callback on this pin. You can pass a
|
|
// nil func to unset the pin change interrupt. If you do so, the change
|
|
// parameter is ignored and can be set to any value (such as 0).
|
|
func (p Pin) SetInterrupt(change PinChange, callback func(Pin)) error {
|
|
// Some variables to easily check whether a channel was already configured
|
|
// as an event channel for the given pin.
|
|
// This is not just an optimization, this is requred: the datasheet says
|
|
// that configuring more than one channel for a given pin results in
|
|
// unpredictable behavior.
|
|
expectedConfigMask := uint32(nrf.GPIOTE_CONFIG_MODE_Msk | nrf.GPIOTE_CONFIG_PSEL_Msk)
|
|
expectedConfig := nrf.GPIOTE_CONFIG_MODE_Event<<nrf.GPIOTE_CONFIG_MODE_Pos | uint32(p)<<nrf.GPIOTE_CONFIG_PSEL_Pos
|
|
|
|
foundChannel := false
|
|
for i := range nrf.GPIOTE.CONFIG {
|
|
config := nrf.GPIOTE.CONFIG[i].Get()
|
|
if config == 0 || config&expectedConfigMask == expectedConfig {
|
|
// Found an empty GPIOTE channel or one that was already configured
|
|
// for this pin.
|
|
if callback == nil {
|
|
// Disable this channel.
|
|
nrf.GPIOTE.INTENCLR.Set(uint32(1 << uint(i)))
|
|
pinCallbacks[i] = nil
|
|
return nil
|
|
}
|
|
// Enable this channel with the given callback.
|
|
nrf.GPIOTE.INTENCLR.Set(uint32(1 << uint(i)))
|
|
nrf.GPIOTE.CONFIG[i].Set(nrf.GPIOTE_CONFIG_MODE_Event<<nrf.GPIOTE_CONFIG_MODE_Pos |
|
|
uint32(p)<<nrf.GPIOTE_CONFIG_PSEL_Pos |
|
|
uint32(change)<<nrf.GPIOTE_CONFIG_POLARITY_Pos)
|
|
pinCallbacks[i] = callback
|
|
nrf.GPIOTE.INTENSET.Set(uint32(1 << uint(i)))
|
|
foundChannel = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundChannel {
|
|
return ErrNoPinChangeChannel
|
|
}
|
|
|
|
// Set and enable the GPIOTE interrupt. It's not a problem if this happens
|
|
// more than once.
|
|
interrupt.New(nrf.IRQ_GPIOTE, func(interrupt.Interrupt) {
|
|
for i := range nrf.GPIOTE.EVENTS_IN {
|
|
if nrf.GPIOTE.EVENTS_IN[i].Get() != 0 {
|
|
nrf.GPIOTE.EVENTS_IN[i].Set(0)
|
|
pin := Pin((nrf.GPIOTE.CONFIG[i].Get() & nrf.GPIOTE_CONFIG_PSEL_Msk) >> nrf.GPIOTE_CONFIG_PSEL_Pos)
|
|
pinCallbacks[i](pin)
|
|
}
|
|
}
|
|
}).Enable()
|
|
|
|
// Everything was configured correctly.
|
|
return nil
|
|
}
|
|
|
|
// UART on the NRF.
|
|
type UART struct {
|
|
Buffer *RingBuffer
|
|
}
|
|
|
|
// UART
|
|
var (
|
|
// UART0 is the hardware UART on the NRF SoC.
|
|
_UART0 = UART{Buffer: NewRingBuffer()}
|
|
UART0 = &_UART0
|
|
)
|
|
|
|
// Configure the UART.
|
|
func (uart *UART) Configure(config UARTConfig) {
|
|
// Default baud rate to 115200.
|
|
if config.BaudRate == 0 {
|
|
config.BaudRate = 115200
|
|
}
|
|
|
|
uart.SetBaudRate(config.BaudRate)
|
|
|
|
// Set TX and RX pins
|
|
if config.TX == 0 && config.RX == 0 {
|
|
// Use default pins
|
|
uart.setPins(UART_TX_PIN, UART_RX_PIN)
|
|
} else {
|
|
uart.setPins(config.TX, config.RX)
|
|
}
|
|
|
|
nrf.UART0.ENABLE.Set(nrf.UART_ENABLE_ENABLE_Enabled)
|
|
nrf.UART0.TASKS_STARTTX.Set(1)
|
|
nrf.UART0.TASKS_STARTRX.Set(1)
|
|
nrf.UART0.INTENSET.Set(nrf.UART_INTENSET_RXDRDY_Msk)
|
|
|
|
// Enable RX IRQ.
|
|
intr := interrupt.New(nrf.IRQ_UART0, _UART0.handleInterrupt)
|
|
intr.SetPriority(0xc0) // low priority
|
|
intr.Enable()
|
|
}
|
|
|
|
// SetBaudRate sets the communication speed for the UART.
|
|
func (uart *UART) SetBaudRate(br uint32) {
|
|
// Magic: calculate 'baudrate' register from the input number.
|
|
// Every value listed in the datasheet will be converted to the
|
|
// correct register value, except for 192600. I suspect the value
|
|
// listed in the nrf52 datasheet (0x0EBED000) is incorrectly rounded
|
|
// and should be 0x0EBEE000, as the nrf51 datasheet lists the
|
|
// nonrounded value 0x0EBEDFA4.
|
|
// Some background:
|
|
// https://devzone.nordicsemi.com/f/nordic-q-a/391/uart-baudrate-register-values/2046#2046
|
|
rate := uint32((uint64(br/400)*uint64(400*0xffffffff/16000000) + 0x800) & 0xffffff000)
|
|
|
|
nrf.UART0.BAUDRATE.Set(rate)
|
|
}
|
|
|
|
// WriteByte writes a byte of data to the UART.
|
|
func (uart *UART) WriteByte(c byte) error {
|
|
nrf.UART0.EVENTS_TXDRDY.Set(0)
|
|
nrf.UART0.TXD.Set(uint32(c))
|
|
for nrf.UART0.EVENTS_TXDRDY.Get() == 0 {
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (uart *UART) handleInterrupt(interrupt.Interrupt) {
|
|
if nrf.UART0.EVENTS_RXDRDY.Get() != 0 {
|
|
uart.Receive(byte(nrf.UART0.RXD.Get()))
|
|
nrf.UART0.EVENTS_RXDRDY.Set(0x0)
|
|
}
|
|
}
|
|
|
|
// I2C on the NRF.
|
|
type I2C struct {
|
|
Bus nrf.TWI_Type
|
|
}
|
|
|
|
// There are 2 I2C interfaces on the NRF.
|
|
var (
|
|
I2C0 = (*I2C)(unsafe.Pointer(nrf.TWI0))
|
|
I2C1 = (*I2C)(unsafe.Pointer(nrf.TWI1))
|
|
)
|
|
|
|
// I2CConfig is used to store config info for I2C.
|
|
type I2CConfig struct {
|
|
Frequency uint32
|
|
SCL Pin
|
|
SDA Pin
|
|
}
|
|
|
|
// Configure is intended to setup the I2C interface.
|
|
func (i2c *I2C) Configure(config I2CConfig) error {
|
|
|
|
i2c.Bus.ENABLE.Set(nrf.TWI_ENABLE_ENABLE_Disabled)
|
|
|
|
// Default I2C bus speed is 100 kHz.
|
|
if config.Frequency == 0 {
|
|
config.Frequency = 100 * KHz
|
|
}
|
|
// Default I2C pins if not set.
|
|
if config.SDA == 0 && config.SCL == 0 {
|
|
config.SDA = SDA_PIN
|
|
config.SCL = SCL_PIN
|
|
}
|
|
|
|
// do config
|
|
sclPort, sclPin := config.SCL.getPortPin()
|
|
sclPort.PIN_CNF[sclPin].Set((nrf.GPIO_PIN_CNF_DIR_Input << nrf.GPIO_PIN_CNF_DIR_Pos) |
|
|
(nrf.GPIO_PIN_CNF_INPUT_Connect << nrf.GPIO_PIN_CNF_INPUT_Pos) |
|
|
(nrf.GPIO_PIN_CNF_PULL_Pullup << nrf.GPIO_PIN_CNF_PULL_Pos) |
|
|
(nrf.GPIO_PIN_CNF_DRIVE_S0D1 << nrf.GPIO_PIN_CNF_DRIVE_Pos) |
|
|
(nrf.GPIO_PIN_CNF_SENSE_Disabled << nrf.GPIO_PIN_CNF_SENSE_Pos))
|
|
|
|
sdaPort, sdaPin := config.SDA.getPortPin()
|
|
sdaPort.PIN_CNF[sdaPin].Set((nrf.GPIO_PIN_CNF_DIR_Input << nrf.GPIO_PIN_CNF_DIR_Pos) |
|
|
(nrf.GPIO_PIN_CNF_INPUT_Connect << nrf.GPIO_PIN_CNF_INPUT_Pos) |
|
|
(nrf.GPIO_PIN_CNF_PULL_Pullup << nrf.GPIO_PIN_CNF_PULL_Pos) |
|
|
(nrf.GPIO_PIN_CNF_DRIVE_S0D1 << nrf.GPIO_PIN_CNF_DRIVE_Pos) |
|
|
(nrf.GPIO_PIN_CNF_SENSE_Disabled << nrf.GPIO_PIN_CNF_SENSE_Pos))
|
|
|
|
if config.Frequency >= 400*KHz {
|
|
i2c.Bus.FREQUENCY.Set(nrf.TWI_FREQUENCY_FREQUENCY_K400)
|
|
} else {
|
|
i2c.Bus.FREQUENCY.Set(nrf.TWI_FREQUENCY_FREQUENCY_K100)
|
|
}
|
|
|
|
i2c.setPins(config.SCL, config.SDA)
|
|
|
|
i2c.Bus.ENABLE.Set(nrf.TWI_ENABLE_ENABLE_Enabled)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Tx does a single I2C transaction at the specified address.
|
|
// It clocks out the given address, writes the bytes in w, reads back len(r)
|
|
// bytes and stores them in r, and generates a stop condition on the bus.
|
|
func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) {
|
|
|
|
// Tricky stop condition.
|
|
// After reads, the stop condition is generated implicitly with a shortcut.
|
|
// After writes not followed by reads and in the case of errors, stop must be generated explicitly.
|
|
|
|
i2c.Bus.ADDRESS.Set(uint32(addr))
|
|
|
|
if len(w) != 0 {
|
|
i2c.Bus.TASKS_STARTTX.Set(1) // start transmission for writing
|
|
for _, b := range w {
|
|
if err = i2c.writeByte(b); err != nil {
|
|
i2c.signalStop()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(r) != 0 {
|
|
// To trigger suspend task when a byte is received
|
|
i2c.Bus.SHORTS.Set(nrf.TWI_SHORTS_BB_SUSPEND)
|
|
i2c.Bus.TASKS_STARTRX.Set(1) // re-start transmission for reading
|
|
for i := range r { // read each char
|
|
if i+1 == len(r) {
|
|
// To trigger stop task when last byte is received, set before resume task.
|
|
i2c.Bus.SHORTS.Set(nrf.TWI_SHORTS_BB_STOP)
|
|
}
|
|
if i > 0 {
|
|
i2c.Bus.TASKS_RESUME.Set(1) // re-start transmission for reading
|
|
}
|
|
if r[i], err = i2c.readByte(); err != nil {
|
|
i2c.Bus.SHORTS.Set(nrf.TWI_SHORTS_BB_SUSPEND_Disabled)
|
|
i2c.signalStop()
|
|
return
|
|
}
|
|
}
|
|
i2c.Bus.SHORTS.Set(nrf.TWI_SHORTS_BB_SUSPEND_Disabled)
|
|
}
|
|
|
|
// Stop explicitly when no reads were executed, stoping unconditionally would be a mistake.
|
|
// It may execute after I2C peripheral has already been stopped by the shortcut in the read block,
|
|
// so stop task will trigger first thing in a subsequent transaction, hanging it.
|
|
if len(r) == 0 {
|
|
i2c.signalStop()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// signalStop sends a stop signal to the I2C peripheral and waits for confirmation.
|
|
func (i2c *I2C) signalStop() {
|
|
i2c.Bus.TASKS_STOP.Set(1)
|
|
for i2c.Bus.EVENTS_STOPPED.Get() == 0 {
|
|
}
|
|
i2c.Bus.EVENTS_STOPPED.Set(0)
|
|
}
|
|
|
|
// writeByte writes a single byte to the I2C bus and waits for confirmation.
|
|
func (i2c *I2C) writeByte(data byte) error {
|
|
i2c.Bus.TXD.Set(uint32(data))
|
|
for i2c.Bus.EVENTS_TXDSENT.Get() == 0 {
|
|
if e := i2c.Bus.EVENTS_ERROR.Get(); e != 0 {
|
|
i2c.Bus.EVENTS_ERROR.Set(0)
|
|
return errI2CBusError
|
|
}
|
|
}
|
|
i2c.Bus.EVENTS_TXDSENT.Set(0)
|
|
return nil
|
|
}
|
|
|
|
// readByte reads a single byte from the I2C bus when it is ready.
|
|
func (i2c *I2C) readByte() (byte, error) {
|
|
for i2c.Bus.EVENTS_RXDREADY.Get() == 0 {
|
|
if e := i2c.Bus.EVENTS_ERROR.Get(); e != 0 {
|
|
i2c.Bus.EVENTS_ERROR.Set(0)
|
|
return 0, errI2CBusError
|
|
}
|
|
}
|
|
i2c.Bus.EVENTS_RXDREADY.Set(0)
|
|
return byte(i2c.Bus.RXD.Get()), nil
|
|
}
|
|
|
|
var rngStarted = false
|
|
|
|
// GetRNG returns 32 bits of non-deterministic random data based on internal thermal noise.
|
|
// According to Nordic's documentation, the random output is suitable for cryptographic purposes.
|
|
func GetRNG() (ret uint32, err error) {
|
|
// There's no apparent way to check the status of the RNG peripheral's task, so simply start it
|
|
// to avoid deadlocking while waiting for output.
|
|
if !rngStarted {
|
|
nrf.RNG.TASKS_START.Set(1)
|
|
nrf.RNG.SetCONFIG_DERCEN(nrf.RNG_CONFIG_DERCEN_Enabled)
|
|
rngStarted = true
|
|
}
|
|
|
|
// The RNG returns one byte at a time, so stack up four bytes into a single uint32 for return.
|
|
for i := 0; i < 4; i++ {
|
|
// Wait for data to be ready.
|
|
for nrf.RNG.EVENTS_VALRDY.Get() == 0 {
|
|
}
|
|
// Append random byte to output.
|
|
ret = (ret << 8) ^ nrf.RNG.GetVALUE()
|
|
// Unset the EVENTS_VALRDY register to avoid reading the same random output twice.
|
|
nrf.RNG.EVENTS_VALRDY.Set(0)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// ReadTemperature reads the silicon die temperature of the chip. The return
|
|
// value is in milli-celsius.
|
|
func ReadTemperature() int32 {
|
|
nrf.TEMP.TASKS_START.Set(1)
|
|
for nrf.TEMP.EVENTS_DATARDY.Get() == 0 {
|
|
}
|
|
temp := int32(nrf.TEMP.TEMP.Get()) * 250 // the returned value is in units of 0.25°C
|
|
nrf.TEMP.EVENTS_DATARDY.Set(0)
|
|
return temp
|
|
}
|
|
|
|
const memoryStart = 0x0
|
|
|
|
// compile-time check for ensuring we fulfill BlockDevice interface
|
|
var _ BlockDevice = flashBlockDevice{}
|
|
|
|
var Flash flashBlockDevice
|
|
|
|
type flashBlockDevice struct {
|
|
}
|
|
|
|
// ReadAt reads the given number of bytes from the block device.
|
|
func (f flashBlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
|
|
if FlashDataStart()+uintptr(off)+uintptr(len(p)) > FlashDataEnd() {
|
|
return 0, errFlashCannotReadPastEOF
|
|
}
|
|
|
|
data := unsafe.Slice((*byte)(unsafe.Pointer(FlashDataStart()+uintptr(off))), len(p))
|
|
copy(p, data)
|
|
|
|
return len(p), nil
|
|
}
|
|
|
|
// WriteAt writes the given number of bytes to the block device.
|
|
// Only double-word (64 bits) length data can be programmed. See rm0461 page 78.
|
|
// If the length of p is not long enough it will be padded with 0xFF bytes.
|
|
// This method assumes that the destination is already erased.
|
|
func (f flashBlockDevice) WriteAt(p []byte, off int64) (n int, err error) {
|
|
if FlashDataStart()+uintptr(off)+uintptr(len(p)) > FlashDataEnd() {
|
|
return 0, errFlashCannotWritePastEOF
|
|
}
|
|
|
|
address := FlashDataStart() + uintptr(off)
|
|
padded := f.pad(p)
|
|
|
|
waitWhileFlashBusy()
|
|
|
|
nrf.NVMC.SetCONFIG_WEN(nrf.NVMC_CONFIG_WEN_Wen)
|
|
defer nrf.NVMC.SetCONFIG_WEN(nrf.NVMC_CONFIG_WEN_Ren)
|
|
|
|
for j := 0; j < len(padded); j += int(f.WriteBlockSize()) {
|
|
// write word
|
|
*(*uint32)(unsafe.Pointer(address)) = binary.LittleEndian.Uint32(padded[j : j+int(f.WriteBlockSize())])
|
|
address += uintptr(f.WriteBlockSize())
|
|
waitWhileFlashBusy()
|
|
}
|
|
|
|
return len(padded), nil
|
|
}
|
|
|
|
// Size returns the number of bytes in this block device.
|
|
func (f flashBlockDevice) Size() int64 {
|
|
return int64(FlashDataEnd() - FlashDataStart())
|
|
}
|
|
|
|
const writeBlockSize = 4
|
|
|
|
// WriteBlockSize returns the block size in which data can be written to
|
|
// memory. It can be used by a client to optimize writes, non-aligned writes
|
|
// should always work correctly.
|
|
func (f flashBlockDevice) WriteBlockSize() int64 {
|
|
return writeBlockSize
|
|
}
|
|
|
|
// EraseBlockSize returns the smallest erasable area on this particular chip
|
|
// in bytes. This is used for the block size in EraseBlocks.
|
|
// It must be a power of two, and may be as small as 1. A typical size is 4096.
|
|
func (f flashBlockDevice) EraseBlockSize() int64 {
|
|
return eraseBlockSize()
|
|
}
|
|
|
|
// EraseBlocks erases the given number of blocks. An implementation may
|
|
// transparently coalesce ranges of blocks into larger bundles if the chip
|
|
// supports this. The start and len parameters are in block numbers, use
|
|
// EraseBlockSize to map addresses to blocks.
|
|
func (f flashBlockDevice) EraseBlocks(start, len int64) error {
|
|
address := FlashDataStart() + uintptr(start*f.EraseBlockSize())
|
|
waitWhileFlashBusy()
|
|
|
|
nrf.NVMC.SetCONFIG_WEN(nrf.NVMC_CONFIG_WEN_Een)
|
|
defer nrf.NVMC.SetCONFIG_WEN(nrf.NVMC_CONFIG_WEN_Ren)
|
|
|
|
for i := start; i < start+len; i++ {
|
|
nrf.NVMC.ERASEPAGE.Set(uint32(address))
|
|
waitWhileFlashBusy()
|
|
address += uintptr(f.EraseBlockSize())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// pad data if needed so it is long enough for correct byte alignment on writes.
|
|
func (f flashBlockDevice) pad(p []byte) []byte {
|
|
paddingNeeded := f.WriteBlockSize() - (int64(len(p)) % f.WriteBlockSize())
|
|
if paddingNeeded == 0 {
|
|
return p
|
|
}
|
|
|
|
padding := bytes.Repeat([]byte{0xff}, int(paddingNeeded))
|
|
return append(p, padding...)
|
|
}
|
|
|
|
func waitWhileFlashBusy() {
|
|
for nrf.NVMC.GetREADY() != nrf.NVMC_READY_READY_Ready {
|
|
}
|
|
}
|