diff options
| author | Rasmus Melchior Jacobsen <[email protected]> | 2023-05-24 12:17:12 +0200 |
|---|---|---|
| committer | Rasmus Melchior Jacobsen <[email protected]> | 2023-05-25 20:07:41 +0200 |
| commit | 0e90e98e9b477302a3cd2550dbd438907ed10ca6 (patch) | |
| tree | 4ec2c5d7bce91ac44659ad4ebc93a04ee7966d1b /embassy-stm32/src/flash/common.rs | |
| parent | 06f5c309c06e61790b24a7bfddad1324741b767f (diff) | |
stm32: Add async flash write/erase to f4
Diffstat (limited to 'embassy-stm32/src/flash/common.rs')
| -rw-r--r-- | embassy-stm32/src/flash/common.rs | 255 |
1 files changed, 141 insertions, 114 deletions
diff --git a/embassy-stm32/src/flash/common.rs b/embassy-stm32/src/flash/common.rs index 432c8a43b..990104a38 100644 --- a/embassy-stm32/src/flash/common.rs +++ b/embassy-stm32/src/flash/common.rs | |||
| @@ -1,44 +1,55 @@ | |||
| 1 | use atomic_polyfill::{fence, Ordering}; | 1 | use atomic_polyfill::{fence, Ordering}; |
| 2 | use embassy_cortex_m::interrupt::InterruptExt; | ||
| 3 | use embassy_futures::block_on; | ||
| 2 | use embassy_hal_common::drop::OnDrop; | 4 | use embassy_hal_common::drop::OnDrop; |
| 3 | use embassy_hal_common::{into_ref, PeripheralRef}; | 5 | use embassy_hal_common::{into_ref, PeripheralRef}; |
| 4 | 6 | use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; | |
| 5 | use super::{family, Error, FlashLayout, FlashRegion, FlashSector, FLASH_BASE, FLASH_SIZE, MAX_ERASE_SIZE, WRITE_SIZE}; | 7 | use embassy_sync::mutex::Mutex; |
| 6 | use crate::flash::FlashBank; | 8 | use stm32_metapac::FLASH_BASE; |
| 9 | |||
| 10 | use super::{ | ||
| 11 | ensure_sector_aligned, family, get_sector, Error, FlashLayout, FlashRegion, FLASH_SIZE, MAX_ERASE_SIZE, READ_SIZE, | ||
| 12 | WRITE_SIZE, | ||
| 13 | }; | ||
| 14 | use crate::peripherals::FLASH; | ||
| 7 | use crate::Peripheral; | 15 | use crate::Peripheral; |
| 8 | 16 | ||
| 9 | pub struct Flash<'d> { | 17 | pub struct Flash<'d> { |
| 10 | inner: PeripheralRef<'d, crate::peripherals::FLASH>, | 18 | pub(crate) inner: PeripheralRef<'d, FLASH>, |
| 11 | } | 19 | } |
| 12 | 20 | ||
| 21 | pub(crate) static REGION_ACCESS: Mutex<CriticalSectionRawMutex, ()> = Mutex::new(()); | ||
| 22 | |||
| 13 | impl<'d> Flash<'d> { | 23 | impl<'d> Flash<'d> { |
| 14 | pub fn new(p: impl Peripheral<P = crate::peripherals::FLASH> + 'd) -> Self { | 24 | pub fn new(p: impl Peripheral<P = FLASH> + 'd, irq: impl Peripheral<P = crate::interrupt::FLASH> + 'd) -> Self { |
| 15 | into_ref!(p); | 25 | into_ref!(p, irq); |
| 26 | |||
| 27 | irq.set_handler(family::on_interrupt); | ||
| 28 | irq.unpend(); | ||
| 29 | irq.enable(); | ||
| 30 | |||
| 16 | Self { inner: p } | 31 | Self { inner: p } |
| 17 | } | 32 | } |
| 18 | 33 | ||
| 19 | pub fn into_regions(self) -> FlashLayout<'d> { | 34 | pub fn into_regions(self) -> FlashLayout<'d> { |
| 20 | family::set_default_layout(); | 35 | family::set_default_layout(); |
| 21 | FlashLayout::new(self.release()) | 36 | FlashLayout::new(self.inner) |
| 22 | } | 37 | } |
| 23 | 38 | ||
| 24 | pub fn blocking_read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { | 39 | pub fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { |
| 25 | blocking_read(FLASH_BASE as u32, FLASH_SIZE as u32, offset, bytes) | 40 | read_blocking(FLASH_BASE as u32, FLASH_SIZE as u32, offset, bytes) |
| 26 | } | 41 | } |
| 27 | 42 | ||
| 28 | pub fn blocking_write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Error> { | 43 | pub fn write_blocking(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Error> { |
| 29 | unsafe { blocking_write_chunked(FLASH_BASE as u32, FLASH_SIZE as u32, offset, bytes) } | 44 | unsafe { write_chunked_blocking(FLASH_BASE as u32, FLASH_SIZE as u32, offset, bytes) } |
| 30 | } | 45 | } |
| 31 | 46 | ||
| 32 | pub fn blocking_erase(&mut self, from: u32, to: u32) -> Result<(), Error> { | 47 | pub fn erase_blocking(&mut self, from: u32, to: u32) -> Result<(), Error> { |
| 33 | unsafe { blocking_erase_sectored(FLASH_BASE as u32, from, to) } | 48 | unsafe { erase_sectored_blocking(FLASH_BASE as u32, from, to) } |
| 34 | } | ||
| 35 | |||
| 36 | pub(crate) fn release(self) -> PeripheralRef<'d, crate::peripherals::FLASH> { | ||
| 37 | unsafe { self.inner.clone_unchecked() } | ||
| 38 | } | 49 | } |
| 39 | } | 50 | } |
| 40 | 51 | ||
| 41 | pub(super) fn blocking_read(base: u32, size: u32, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { | 52 | pub(super) fn read_blocking(base: u32, size: u32, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { |
| 42 | if offset + bytes.len() as u32 > size { | 53 | if offset + bytes.len() as u32 > size { |
| 43 | return Err(Error::Size); | 54 | return Err(Error::Size); |
| 44 | } | 55 | } |
| @@ -49,7 +60,7 @@ pub(super) fn blocking_read(base: u32, size: u32, offset: u32, bytes: &mut [u8]) | |||
| 49 | Ok(()) | 60 | Ok(()) |
| 50 | } | 61 | } |
| 51 | 62 | ||
| 52 | pub(super) unsafe fn blocking_write_chunked(base: u32, size: u32, offset: u32, bytes: &[u8]) -> Result<(), Error> { | 63 | pub(super) unsafe fn write_chunked_blocking(base: u32, size: u32, offset: u32, bytes: &[u8]) -> Result<(), Error> { |
| 53 | if offset + bytes.len() as u32 > size { | 64 | if offset + bytes.len() as u32 > size { |
| 54 | return Err(Error::Size); | 65 | return Err(Error::Size); |
| 55 | } | 66 | } |
| @@ -61,44 +72,31 @@ pub(super) unsafe fn blocking_write_chunked(base: u32, size: u32, offset: u32, b | |||
| 61 | trace!("Writing {} bytes at 0x{:x}", bytes.len(), address); | 72 | trace!("Writing {} bytes at 0x{:x}", bytes.len(), address); |
| 62 | 73 | ||
| 63 | for chunk in bytes.chunks(WRITE_SIZE) { | 74 | for chunk in bytes.chunks(WRITE_SIZE) { |
| 64 | critical_section::with(|_| { | 75 | family::clear_all_err(); |
| 65 | family::clear_all_err(); | 76 | fence(Ordering::SeqCst); |
| 66 | fence(Ordering::SeqCst); | 77 | family::unlock(); |
| 67 | family::unlock(); | 78 | fence(Ordering::SeqCst); |
| 79 | family::enable_blocking_write(); | ||
| 80 | fence(Ordering::SeqCst); | ||
| 81 | |||
| 82 | let _on_drop = OnDrop::new(|| { | ||
| 83 | family::disable_blocking_write(); | ||
| 68 | fence(Ordering::SeqCst); | 84 | fence(Ordering::SeqCst); |
| 69 | family::begin_write(); | 85 | family::lock(); |
| 70 | fence(Ordering::SeqCst); | 86 | }); |
| 71 | |||
| 72 | let _on_drop = OnDrop::new(|| { | ||
| 73 | family::end_write(); | ||
| 74 | fence(Ordering::SeqCst); | ||
| 75 | family::lock(); | ||
| 76 | }); | ||
| 77 | 87 | ||
| 78 | family::blocking_write(address, chunk.try_into().unwrap()) | 88 | family::write_blocking(address, chunk.try_into().unwrap())?; |
| 79 | })?; | ||
| 80 | address += WRITE_SIZE as u32; | 89 | address += WRITE_SIZE as u32; |
| 81 | } | 90 | } |
| 82 | Ok(()) | 91 | Ok(()) |
| 83 | } | 92 | } |
| 84 | 93 | ||
| 85 | pub(super) unsafe fn blocking_erase_sectored(base: u32, from: u32, to: u32) -> Result<(), Error> { | 94 | pub(super) unsafe fn erase_sectored_blocking(base: u32, from: u32, to: u32) -> Result<(), Error> { |
| 86 | let start_address = base + from; | 95 | let start_address = base + from; |
| 87 | let end_address = base + to; | 96 | let end_address = base + to; |
| 88 | let regions = family::get_flash_regions(); | 97 | let regions = family::get_flash_regions(); |
| 89 | 98 | ||
| 90 | // Test if the address range is aligned at sector base addresses | 99 | ensure_sector_aligned(start_address, end_address, regions)?; |
| 91 | let mut address = start_address; | ||
| 92 | while address < end_address { | ||
| 93 | let sector = get_sector(address, regions); | ||
| 94 | if sector.start != address { | ||
| 95 | return Err(Error::Unaligned); | ||
| 96 | } | ||
| 97 | address += sector.size; | ||
| 98 | } | ||
| 99 | if address != end_address { | ||
| 100 | return Err(Error::Unaligned); | ||
| 101 | } | ||
| 102 | 100 | ||
| 103 | trace!("Erasing from 0x{:x} to 0x{:x}", start_address, end_address); | 101 | trace!("Erasing from 0x{:x} to 0x{:x}", start_address, end_address); |
| 104 | 102 | ||
| @@ -107,104 +105,146 @@ pub(super) unsafe fn blocking_erase_sectored(base: u32, from: u32, to: u32) -> R | |||
| 107 | let sector = get_sector(address, regions); | 105 | let sector = get_sector(address, regions); |
| 108 | trace!("Erasing sector: {:?}", sector); | 106 | trace!("Erasing sector: {:?}", sector); |
| 109 | 107 | ||
| 110 | critical_section::with(|_| { | 108 | family::clear_all_err(); |
| 111 | family::clear_all_err(); | 109 | fence(Ordering::SeqCst); |
| 112 | fence(Ordering::SeqCst); | 110 | family::unlock(); |
| 113 | family::unlock(); | 111 | fence(Ordering::SeqCst); |
| 114 | fence(Ordering::SeqCst); | ||
| 115 | 112 | ||
| 116 | let _on_drop = OnDrop::new(|| { | 113 | let _on_drop = OnDrop::new(|| family::lock()); |
| 117 | family::lock(); | ||
| 118 | }); | ||
| 119 | 114 | ||
| 120 | family::blocking_erase_sector(§or) | 115 | family::erase_sector_blocking(§or)?; |
| 121 | })?; | ||
| 122 | address += sector.size; | 116 | address += sector.size; |
| 123 | } | 117 | } |
| 124 | Ok(()) | 118 | Ok(()) |
| 125 | } | 119 | } |
| 126 | 120 | ||
| 127 | pub(crate) fn get_sector(address: u32, regions: &[&FlashRegion]) -> FlashSector { | 121 | impl embedded_storage::nor_flash::ErrorType for Flash<'_> { |
| 128 | let mut current_bank = FlashBank::Bank1; | 122 | type Error = Error; |
| 129 | let mut bank_offset = 0; | 123 | } |
| 130 | for region in regions { | ||
| 131 | if region.bank != current_bank { | ||
| 132 | current_bank = region.bank; | ||
| 133 | bank_offset = 0; | ||
| 134 | } | ||
| 135 | 124 | ||
| 136 | if address < region.end() { | 125 | impl embedded_storage::nor_flash::ReadNorFlash for Flash<'_> { |
| 137 | let index_in_region = (address - region.base) / region.erase_size; | 126 | const READ_SIZE: usize = READ_SIZE; |
| 138 | return FlashSector { | ||
| 139 | bank: region.bank, | ||
| 140 | index_in_bank: bank_offset + index_in_region as u8, | ||
| 141 | start: region.base + index_in_region * region.erase_size, | ||
| 142 | size: region.erase_size, | ||
| 143 | }; | ||
| 144 | } | ||
| 145 | 127 | ||
| 146 | bank_offset += region.sectors(); | 128 | fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { |
| 129 | self.read(offset, bytes) | ||
| 147 | } | 130 | } |
| 148 | 131 | ||
| 149 | panic!("Flash sector not found"); | 132 | fn capacity(&self) -> usize { |
| 133 | FLASH_SIZE | ||
| 134 | } | ||
| 150 | } | 135 | } |
| 151 | 136 | ||
| 152 | impl FlashRegion { | 137 | impl embedded_storage::nor_flash::NorFlash for Flash<'_> { |
| 153 | pub fn blocking_read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { | 138 | const WRITE_SIZE: usize = WRITE_SIZE; |
| 154 | blocking_read(self.base, self.size, offset, bytes) | 139 | const ERASE_SIZE: usize = MAX_ERASE_SIZE; |
| 140 | |||
| 141 | fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { | ||
| 142 | self.write_blocking(offset, bytes) | ||
| 143 | } | ||
| 144 | |||
| 145 | fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { | ||
| 146 | self.erase_blocking(from, to) | ||
| 155 | } | 147 | } |
| 148 | } | ||
| 149 | |||
| 150 | #[cfg(feature = "nightly")] | ||
| 151 | impl embedded_storage_async::nor_flash::ReadNorFlash for Flash<'_> { | ||
| 152 | const READ_SIZE: usize = READ_SIZE; | ||
| 156 | 153 | ||
| 157 | pub fn blocking_write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Error> { | 154 | async fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { |
| 158 | unsafe { blocking_write_chunked(self.base, self.size, offset, bytes) } | 155 | self.read(offset, bytes) |
| 159 | } | 156 | } |
| 160 | 157 | ||
| 161 | pub fn blocking_erase(&mut self, from: u32, to: u32) -> Result<(), Error> { | 158 | fn capacity(&self) -> usize { |
| 162 | unsafe { blocking_erase_sectored(self.base, from, to) } | 159 | FLASH_SIZE |
| 163 | } | 160 | } |
| 164 | } | 161 | } |
| 165 | 162 | ||
| 166 | impl embedded_storage::nor_flash::ErrorType for Flash<'_> { | 163 | pub struct BlockingFlashRegion<'d, const WRITE_SIZE: u32, const ERASE_SIZE: u32>( |
| 164 | &'static FlashRegion, | ||
| 165 | PeripheralRef<'d, FLASH>, | ||
| 166 | ); | ||
| 167 | |||
| 168 | impl<const WRITE_SIZE: u32, const ERASE_SIZE: u32> BlockingFlashRegion<'_, WRITE_SIZE, ERASE_SIZE> { | ||
| 169 | pub fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { | ||
| 170 | read_blocking(self.0.base, self.0.size, offset, bytes) | ||
| 171 | } | ||
| 172 | |||
| 173 | pub fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Error> { | ||
| 174 | let _guard = block_on(REGION_ACCESS.lock()); | ||
| 175 | unsafe { write_chunked_blocking(self.0.base, self.0.size, offset, bytes) } | ||
| 176 | } | ||
| 177 | |||
| 178 | pub fn erase(&mut self, from: u32, to: u32) -> Result<(), Error> { | ||
| 179 | let _guard = block_on(REGION_ACCESS.lock()); | ||
| 180 | unsafe { erase_sectored_blocking(self.0.base, from, to) } | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | impl<const WRITE_SIZE: u32, const ERASE_SIZE: u32> embedded_storage::nor_flash::ErrorType | ||
| 185 | for BlockingFlashRegion<'_, WRITE_SIZE, ERASE_SIZE> | ||
| 186 | { | ||
| 167 | type Error = Error; | 187 | type Error = Error; |
| 168 | } | 188 | } |
| 169 | 189 | ||
| 170 | impl embedded_storage::nor_flash::ReadNorFlash for Flash<'_> { | 190 | impl<const WRITE_SIZE: u32, const ERASE_SIZE: u32> embedded_storage::nor_flash::ReadNorFlash |
| 171 | const READ_SIZE: usize = 1; | 191 | for BlockingFlashRegion<'_, WRITE_SIZE, ERASE_SIZE> |
| 192 | { | ||
| 193 | const READ_SIZE: usize = READ_SIZE; | ||
| 172 | 194 | ||
| 173 | fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { | 195 | fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { |
| 174 | self.blocking_read(offset, bytes) | 196 | self.read(offset, bytes) |
| 175 | } | 197 | } |
| 176 | 198 | ||
| 177 | fn capacity(&self) -> usize { | 199 | fn capacity(&self) -> usize { |
| 178 | FLASH_SIZE | 200 | self.0.size as usize |
| 179 | } | 201 | } |
| 180 | } | 202 | } |
| 181 | 203 | ||
| 182 | impl embedded_storage::nor_flash::NorFlash for Flash<'_> { | 204 | impl<const WRITE_SIZE: u32, const ERASE_SIZE: u32> embedded_storage::nor_flash::NorFlash |
| 183 | const WRITE_SIZE: usize = WRITE_SIZE; | 205 | for BlockingFlashRegion<'_, WRITE_SIZE, ERASE_SIZE> |
| 184 | const ERASE_SIZE: usize = MAX_ERASE_SIZE; | 206 | { |
| 207 | const WRITE_SIZE: usize = WRITE_SIZE as usize; | ||
| 208 | const ERASE_SIZE: usize = ERASE_SIZE as usize; | ||
| 185 | 209 | ||
| 186 | fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { | 210 | fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { |
| 187 | self.blocking_write(offset, bytes) | 211 | self.write(offset, bytes) |
| 188 | } | 212 | } |
| 189 | 213 | ||
| 190 | fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { | 214 | fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { |
| 191 | self.blocking_erase(from, to) | 215 | self.erase(from, to) |
| 192 | } | 216 | } |
| 193 | } | 217 | } |
| 194 | 218 | ||
| 195 | foreach_flash_region! { | 219 | foreach_flash_region! { |
| 196 | ($type_name:ident, $write_size:literal, $erase_size:literal) => { | 220 | ($type_name:ident, $write_size:literal, $erase_size:literal) => { |
| 197 | impl crate::_generated::flash_regions::$type_name<'_> { | 221 | paste::paste! { |
| 198 | pub fn blocking_read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { | 222 | pub type [<Blocking $type_name>]<'d> = BlockingFlashRegion<'d, $write_size, $erase_size>; |
| 199 | blocking_read(self.0.base, self.0.size, offset, bytes) | 223 | } |
| 224 | |||
| 225 | impl<'d> crate::_generated::flash_regions::$type_name<'d> { | ||
| 226 | /// Make this flash region work in a blocking context. | ||
| 227 | /// | ||
| 228 | /// SAFETY | ||
| 229 | /// | ||
| 230 | /// This function is unsafe as incorect usage of parallel blocking operations | ||
| 231 | /// on multiple regions may cause a deadlock because each region requires mutual access to the flash. | ||
| 232 | pub unsafe fn into_blocking(self) -> BlockingFlashRegion<'d, $write_size, $erase_size> { | ||
| 233 | BlockingFlashRegion(self.0, self.1) | ||
| 200 | } | 234 | } |
| 201 | 235 | ||
| 202 | pub fn blocking_write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Error> { | 236 | pub fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Error> { |
| 203 | unsafe { blocking_write_chunked(self.0.base, self.0.size, offset, bytes) } | 237 | read_blocking(self.0.base, self.0.size, offset, bytes) |
| 204 | } | 238 | } |
| 205 | 239 | ||
| 206 | pub fn blocking_erase(&mut self, from: u32, to: u32) -> Result<(), Error> { | 240 | pub fn try_write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Error> { |
| 207 | unsafe { blocking_erase_sectored(self.0.base, from, to) } | 241 | let _guard = REGION_ACCESS.try_lock().map_err(|_| Error::TryLockError)?; |
| 242 | unsafe { write_chunked_blocking(self.0.base, self.0.size, offset, bytes) } | ||
| 243 | } | ||
| 244 | |||
| 245 | pub fn try_erase(&mut self, from: u32, to: u32) -> Result<(), Error> { | ||
| 246 | let _guard = REGION_ACCESS.try_lock().map_err(|_| Error::TryLockError)?; | ||
| 247 | unsafe { erase_sectored_blocking(self.0.base, from, to) } | ||
| 208 | } | 248 | } |
| 209 | } | 249 | } |
| 210 | 250 | ||
| @@ -213,28 +253,15 @@ foreach_flash_region! { | |||
| 213 | } | 253 | } |
| 214 | 254 | ||
| 215 | impl embedded_storage::nor_flash::ReadNorFlash for crate::_generated::flash_regions::$type_name<'_> { | 255 | impl embedded_storage::nor_flash::ReadNorFlash for crate::_generated::flash_regions::$type_name<'_> { |
| 216 | const READ_SIZE: usize = 1; | 256 | const READ_SIZE: usize = READ_SIZE; |
| 217 | 257 | ||
| 218 | fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { | 258 | fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { |
| 219 | self.blocking_read(offset, bytes) | 259 | self.read(offset, bytes) |
| 220 | } | 260 | } |
| 221 | 261 | ||
| 222 | fn capacity(&self) -> usize { | 262 | fn capacity(&self) -> usize { |
| 223 | self.0.size as usize | 263 | self.0.size as usize |
| 224 | } | 264 | } |
| 225 | } | 265 | } |
| 226 | |||
| 227 | impl embedded_storage::nor_flash::NorFlash for crate::_generated::flash_regions::$type_name<'_> { | ||
| 228 | const WRITE_SIZE: usize = $write_size; | ||
| 229 | const ERASE_SIZE: usize = $erase_size; | ||
| 230 | |||
| 231 | fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { | ||
| 232 | self.blocking_write(offset, bytes) | ||
| 233 | } | ||
| 234 | |||
| 235 | fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { | ||
| 236 | self.blocking_erase(from, to) | ||
| 237 | } | ||
| 238 | } | ||
| 239 | }; | 266 | }; |
| 240 | } | 267 | } |
