cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Highlighted
Adventurer
Adventurer
1,733 Views
Registered: ‎10-02-2018

How to compare variable environment in u-boot?

Hi,

I compared variable in u-boot. But error occur. Here is u-boot environment in /PetaProject/project-spec/meta-user/recipes-bsp/u-boot/files/platform-top.h:

#define CONFIG_EXTRA_ENV_SETTINGS \
SERIAL_MULTI \
CONSOLE_ARG \
DFU_ALT_INFO \
PSSERIAL0 \
"nc=setenv stdout nc;setenv stdin nc;\0" \
"ethaddr=00:0a:35:00:22:01\0" \
"sdbootdev=0\0" \
"bootenv=uEnv.txt\0" \
"importbootenv=echo \"Importing environment from SD ...\"; " \
"env import -t ${loadbootenv_addr} $filesize\0" \
"loadbootenv=load mmc $sdbootdev:$partid ${loadbootenv_addr} ${bootenv}\0" \
"sd_uEnvtxt_existence_test=test -e mmc $sdbootdev:$partid /uEnv.txt\0" \
"uenvboot=" \
"if run sd_uEnvtxt_existence_test; then " \
"run loadbootenv; " \
"echo Loaded environment from ${bootenv}; " \
"run importbootenv; " \
"fi; " \
"if test -n $uenvcmd; then " \
"echo Running uenvcmd ...; " \
"run uenvcmd; " \
"fi\0" \
"autoload=no\0" \
"imagekernel=1\0" \
"true=1\0" \
"clobstart=0x10000000\0" \
"netstart=0x10000000\0" \
"dtbnetstart=0x11800000\0" \
"loadaddr=0x10000000\0" \
"bootsize=0xf00000\0" \
"bootstart=0x0\0" \
"boot_img=BOOT.BIN\0" \
"load_boot=tftpboot ${clobstart} ${boot_img}\0" \
"update_boot=setenv img boot; setenv psize ${bootsize}; setenv installcmd \"install_boot\"; run load_boot test_img; setenv img; setenv psize; setenv installcmd\0" \
"sd_update_boot=echo Updating boot from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${boot_img} && run install_boot\0" \
"install_boot=sf probe 0 && sf erase ${bootstart} ${bootsize} && " \
"sf write ${clobstart} ${bootstart} ${filesize}\0" \
"bootenvsize=0x40000\0" \
"bootenvstart=0xf00000\0" \
"eraseenv=sf probe 0 && sf erase ${bootenvstart} ${bootenvsize}\0" \
"jffs2_img=rootfs.jffs2\0" \
"load_jffs2=tftpboot ${clobstart} ${jffs2_img}\0" \
"update_jffs2=setenv img jffs2; setenv psize ${jffs2size}; setenv installcmd \"install_jffs2\"; run load_jffs2 test_img; setenv img; setenv psize; setenv installcmd\0" \
"sd_update_jffs2=echo Updating jffs2 from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${jffs2_img} && run install_jffs2\0" \
"install_jffs2=sf probe 0 && sf erase ${jffs2start} ${jffs2size} && " \
"sf write ${clobstart} ${jffs2start} ${filesize}\0" \
"kernelsize=0x1400000\0" \

"imagekernel=1\0" \
"kernelstart=if test ${imagekernel} = 1; then  0xf40000; fi\0" \
"kernel_img=image.ub\0" \
"load_kernel=tftpboot ${clobstart} ${kernel_img}\0" \
"update_kernel=setenv img kernel; setenv psize ${kernelsize}; setenv installcmd \"install_kernel\"; run load_kernel test_crc; setenv img; setenv psize; setenv installcmd\0" \
"sd_update_kernel=echo Updating kernel from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${kernel_img} && run install_kernel\0" \
"install_kernel=sf probe 0 && sf erase ${kernelstart} ${kernelsize} && " \
"sf write ${clobstart} ${kernelstart} ${filesize}\0" \
"cp_kernel2ram=sf probe 0 && sf read ${netstart} ${kernelstart} ${kernelsize}\0" \
"dtb_img=system.dtb\0" \
"load_dtb=tftpboot ${clobstart} ${dtb_img}\0" \
"update_dtb=setenv img dtb; setenv psize ${dtbsize}; setenv installcmd \"install_dtb\"; run load_dtb test_img; setenv img; setenv psize; setenv installcmd\0" \
"sd_update_dtb=echo Updating dtb from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${dtb_img} && run install_dtb\0" \
"loadbootenv_addr=0x00100000\0" \
"fault=echo ${img} image size is greater than allocated place - partition ${img} is NOT UPDATED\0" \
"test_crc=if imi ${clobstart}; then run test_img; else echo ${img} Bad CRC - ${img} is NOT UPDATED; fi\0" \
"test_img=setenv var \"if test ${filesize} -gt ${psize}\\; then run fault\\; else run ${installcmd}\\; fi\"; run var; setenv var\0" \
"netboot=tftpboot ${netstart} ${kernel_img} && bootm\0" \
"default_bootcmd=run cp_kernel2ram && bootm ${netstart}\0" \
""

I plan to boot from 2 different kernel start. But with above red line, error occur once boot from QSPI:

U-Boot 2017.01 (Oct 14 2019 - 14:33:29 +0700) Xilinx ZynqMP ZCU102 rev1.0

I2C: ready
DRAM: 4 GiB
EL Level: EL2
Chip ID: xczu9eg
MMC: sdhci@ff170000: 0 (SD)
zynqmp_qspi_ofdata_to_platdata: CLK 124987500
SF: Detected n25q512a with page size 512 Bytes, erase size 128 KiB, total 128 MiB
*** Warning - bad CRC, using default environment

In: serial
Out: serial
Err: serial
Bootmode: QSPI_MODE
Net: ZYNQ GEM: ff0e0000, phyaddr c, interface rgmii-id

Warning: ethernet@ff0e0000 MAC addresses don't match:
Address in SROM is 01:02:03:04:05:06
Address in environment is 00:0a:35:00:22:01
eth0: ethernet@ff0e0000
U-BOOT for xilinx-zcu102-2017_4

Hit any key to stop autoboot: 0
SF: Detected n25q512a with page size 512 Bytes, erase size 128 KiB, total 128 MiB
sf - SPI flash sub-system

Usage:
sf probe [[bus:]cs] [hz] [mode] - init flash device on given SPI bus
and chip select
sf read addr offset|partition len - read `len' bytes starting at
`offset' or from start of mtd
`partition'to memory at `addr'
sf write addr offset|partition len - write `len' bytes from memory
at `addr' to flash at `offset'
or to start of mtd `partition'
sf erase offset|partition [+]len - erase `len' bytes from `offset'
or from start of mtd `partition'
`+len' round up `len' to block size
sf update addr offset|partition len - erase and write `len' bytes from memory
at `addr' to flash at `offset'
or to start of mtd `partition'
sf protect lock/unlock sector len - protect/unprotect 'len' bytes starting
at address 'sector'

ZynqMP>

Thanks and brgs

 

 

0 Kudos
9 Replies
Highlighted
Explorer
Explorer
1,701 Views
Registered: ‎10-03-2018

Hello @sonminh,

It appears that you are creating an environment symbol, not a line which will be executed as script.  Presumably you hand executed that command and verified that it worked correctly. 

I will show my ignorance and ask where is "${kernelstart}" executed from?  Also, that likely won't work as a command, as the symbol will evaluate to the command "if test ${imagekernel} = 1; then  0xf40000; fi", which has no useful side effect. 

The remainder of the code treats "${kernelstart}" as a constant value.  Perhaps you need to inject the full text as a command into one of the other symbols (which are commands), instead, so that the side-effect value is computed? 

Hopefully this makes sense. 

 

Kind Regards,
Peimann, S. M.
----
Toddlers are the Storm-Troopers of the Great God Entropy.
Physics: Not Just a Good Idea, It's THE LAW.
0 Kudos
Highlighted
Adventurer
Adventurer
1,692 Views
Registered: ‎10-02-2018

Hi Peimann,

May be some mistakes. 

Variable ${kernelstart} is variable symbol, not command. So u-boot will read variable ${kernelstart} to know address of image.ub. I want to variable ${kernelstart} recieve one of two different value by compare variable ${imagekernel} equal or not equal 1. But I don't know implement that. 

Can you talk clearly. 

Thanks  

0 Kudos
Highlighted
Explorer
Explorer
1,683 Views
Registered: ‎10-03-2018

Hello @sonminh,

I will try to write clearly, so please ask questions when I am unclear.

  1. The entire file ".../u-boot/files/platform-top.h" is a list of environment symbols which are embedded into Das U-Boot. 
  2. Das U-Boot expands symbols exactly as written, substituting the right-hand-side for the left-hand-side. 
  3. Das U-Boot expands its "default" boot command sequence unless interrupted.
  4. The "default" command sequence of Das U-Boot is implemented as a series of commands stored in symbols. 
  5. See the "default_bootcmd" at the end of the "../u-boot/files/platform-top.h" listing. 
  6. From what I see in your change, you have modified a symbol which is used as a constant value; it is not an executed command. 
  7. I assume that you manually tested your "if" statement at Das U-Boot command line. 
  8. Perhaps change "default_bootcmd":
    1. "default_bootcmd=imagekernel=1; if test ${imagekernel} = 1; then kernelstart=0xf40000; fi; run cp_kernel2ram && bootm ${netstart}\0"

Good Luck!

Kind Regards,
Peimann, S. M.
----
Toddlers are the Storm-Troopers of the Great God Entropy.
Physics: Not Just a Good Idea, It's THE LAW.
0 Kudos
Highlighted
Adventurer
Adventurer
1,623 Views
Registered: ‎10-02-2018

Hi Peimann,

It is very useful for me. Thanks you very much.

Now, with your help, I can boot from one of two differrent kernel image by use fw_setenv ${imagekernel} equal 1 or not equal 1 from user space.

But i 'm confuse once boot from one of two differrent rootfs because no have variable ${jffs2start} in environment variable. May be linux read address of rootfs paration from device-tree in image.ub to know address of rootfs.jffs2

Do you know how to boot boot from one of two differrent rootfs ?

This my devicetree:

flash@0 {
compatible = "n25q512a", "micron,m25p80";
#address-cells = <0x1>;
#size-cells = <0x1>;
reg = <0x0>;
spi-tx-bus-width = <0x1>;
spi-rx-bus-width = <0x4>;
spi-max-frequency = <0x66ff300>;

partition@0x00000000 {
label = "boot";
reg = <0x0 0xf00000>;
};

partition@0x00f00000 {
label = "bootenv";
reg = <0xf00000 0x40000>;
};

partition@0x00f40000 {
label = "kernel";
reg = <0xf40000 0x1400000>;
};

partition@0x02340000 {
label = "jffs2_3";
reg = <0x2340000 0xf00000>;
};

partition@0x03240000 {
label = "boot_1";
reg = <0x3240000 0xf00000>;
};

partition@0x04140000 {
label = "kernel_1";
reg = <0x4140000 0x1400000>;
};

partition@0x05540000 {
label = "jffs2_6";
reg = <0x5540000 0xf00000>;
};

partition@0x06440000 {
label = "user data";
reg = <0x6440000 0x1400000>;
};

chosen {
bootargs = "earlycon clk_ignore_unused root=mtd:jffs2_3 rw rootfstype=jffs2 rootwait";
stdout-path = "serial0:115200n8";
};

and here is environment variable:

root@xilinx-zcu102-2017_4:~#
root@xilinx-zcu102-2017_4:~# fw_printenv
a=0
autoload=no
baudrate=115200
boot_img=BOOT.BIN
boot_targets=qspi0 <NULL>
bootcmd=run default_bootcmd
bootdelay=4
bootenv=uEnv.txt
bootenvsize=0x40000
bootenvstart=0xf00000
bootsize=0xf00000
bootstart=0x0
clobstart=0x10000000
console=console=ttyPS0,115200
cp_kernel2ram=sf probe 0 && sf read ${netstart} ${kernelstart} ${kernelsize}
default_bootcmd=if test ${imagekernel} = 1; then kernelstart=0xf40000; else kernelstart=0x04140000; fi; run cp_kernel2ram && bootm ${netstart}
dfu_ram=run dfu_ram_info && dfu 0 ram 0
dfu_ram_info=setenv dfu_alt_info image.ub ram $netstart 0x1e00000
dtb_img=system.dtb
dtbnetstart=0x11800000
eraseenv=sf probe 0 && sf erase ${bootenvstart} ${bootenvsize}
ethaddr=00:0a:35:00:22:01
fault=echo ${img} image size is greater than allocated place - partition ${img} is NOT UPDATED
fdtcontroladdr=7ff7f470
gatewayip=192.168.15.1
importbootenv=echo "Importing environment from SD ..."; env import -t ${loadbootenv_addr} $filesize
install_boot=sf probe 0 && sf erase ${bootstart} ${bootsize} && sf write ${clobstart} ${bootstart} ${filesize}
install_jffs2=sf probe 0 && sf erase ${jffs2start} ${jffs2size} && sf write ${clobstart} ${jffs2start} ${filesize}
install_kernel=sf probe 0 && sf erase ${kernelstart} ${kernelsize} && sf write ${clobstart} ${kernelstart} ${filesize}
ipaddr=192.168.15.150
jffs2_img=rootfs.jffs2
kernel_img=image.ub
kernelsize=0x1400000
load_boot=tftpboot ${clobstart} ${boot_img}
load_dtb=tftpboot ${clobstart} ${dtb_img}
load_jffs2=tftpboot ${clobstart} ${jffs2_img}
load_kernel=tftpboot ${clobstart} ${kernel_img}
loadaddr=0x10000000
loadbootenv=load mmc $sdbootdev:$partid ${loadbootenv_addr} ${bootenv}
loadbootenv_addr=0x00100000
modeboot=qspiboot
nc=setenv stdout nc;setenv stdin nc;
netboot=tftpboot ${netstart} ${kernel_img} && bootm
netmask=255.255.255.0
netstart=0x10000000
psserial0=setenv stdout ttyPS0;setenv stdin ttyPS0
sd_uEnvtxt_existence_test=test -e mmc $sdbootdev:$partid /uEnv.txt
sd_update_boot=echo Updating boot from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${boot_img} && run install_boot
sd_update_dtb=echo Updating dtb from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${dtb_img} && run install_dtb
sd_update_jffs2=echo Updating jffs2 from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${jffs2_img} && run install_jffs2
sd_update_kernel=echo Updating kernel from SD; mmcinfo && fatload mmc 0:1 ${clobstart} ${kernel_img} && run install_kernel
sdbootdev=0
serial=setenv stdout serial;setenv stdin serial
setup=setenv partid auto
test_crc=if imi ${clobstart}; then run test_img; else echo ${img} Bad CRC - ${img} is NOT UPDATED; fi
test_img=setenv var "if test ${filesize} -gt ${psize}; then run fault; else run ${installcmd}; fi"; run var; setenv var
thor_ram=run dfu_ram_info && thordown 0 ram 0
true=1
uenvboot=if run sd_uEnvtxt_existence_test; then run loadbootenv; echo Loaded environment from ${bootenv}; run importbootenv; fi; if test -n $uenvcmd; then echo Running uenvcmd ...; run uenvcmd; fi
update_boot=setenv img boot; setenv psize ${bootsize}; setenv installcmd "install_boot"; run load_boot test_img; setenv img; setenv psize; setenv installcmd
update_dtb=setenv img dtb; setenv psize ${dtbsize}; setenv installcmd "install_dtb"; run load_dtb test_img; setenv img; setenv psize; setenv installcmd
update_jffs2=setenv img jffs2; setenv psize ${jffs2size}; setenv installcmd "install_jffs2"; run load_jffs2 test_img; setenv img; setenv psize; setenv installcmd
update_kernel=setenv img kernel; setenv psize ${kernelsize}; setenv installcmd "install_kernel"; run load_kernel test_crc; setenv img; setenv psize; setenv installcmd
serverip=192.168.15.15
imagekernel=0

 

0 Kudos
Highlighted
Explorer
Explorer
1,607 Views
Registered: ‎10-03-2018

Good morning @sonminh,

I believe that you are printing the Das U-Boot variables which are stored on your flash memory.  This may not be the complete variable set that Das U-Boot maintains, although I expect that it is nearly comprehensive. 

You will likely have to establish the symbol jffs2start, in a manner as was done for kernelstart, assuming that you want to boot from a jffs2 stored image. 

[edit] Observe that flash layout is in the kernel device tree - this means that the kernel knows where to find the jffs2 image that it will use.  You may need to override this in the kernel arguments that it boots with, if you use the same device tree for both images. 

Have you looked at all the related commands which are stored in the environment, some for loading images, some for booting, &c.  Many of these symbol values are not defined in the environment provided by default.  You will have to provide them, or manually create them from the command line of Das U-Boot. 

For example:


load_boot=tftpboot ${clobstart} ${boot_img}
load_dtb=tftpboot ${clobstart} ${dtb_img}
load_jffs2=tftpboot ${clobstart} ${jffs2_img}
load_kernel=tftpboot ${clobstart} ${kernel_img}

So - what do you really want to accomplish? 

  1. Multi-boot under user control?
  2. Image update with manual recovery on failure? 
  3. Boot from automatically detected, latest complete boot image?
  4. Something else?

The gentlemen at Das U-Boot have tried to make your life easy by providing commands in the environment, which have nice names telling you what they are about.  You can use these manually from the command line of Das U-Boot, or you can implement them into the automated boot sequence.  Once done, you can remove the parts that you do not want.  In fact, when you are done, you can close off any command-line access to Das U-Boot, should you desire. 

I really am trying to help you, but I also have my own work to deal with. 

Please help me help you. 

Ergo: What do you really want to accomplish?   

Kind Regards,
Peimann, S. M.
----
Toddlers are the Storm-Troopers of the Great God Entropy.
Physics: Not Just a Good Idea, It's THE LAW.
0 Kudos
Highlighted
Adventurer
Adventurer
1,590 Views
Registered: ‎10-02-2018

Thanks Peimann.

You are so kind.

My project is using SW Update follow link to update BOOT.BIN, image.ub, rootfs.jffs2 into flash on zcu102:

https://www.zachpfeffer.com/single-post/Integrate-SWUpdate-into-a-PetaLinux-Project

I have finished integrate SW Update into rootfs and boot successfull, and I can access webserver as image I attached.

I plan to reserve 2 partition on flash: one partition for update and one partition for load rootfs, image.ub, boot.bin.

So, as you talk, with rootfs.jffs2, May be I must need to override this in the kernel arguments in device-tree from user space that it boots with:

chosen {
bootargs = "earlycon clk_ignore_unused root=mtd:jffs2_3 rw rootfstype=jffs2 rootwait";
stdout-path = "serial0:115200n8";
};

I with try research override this in the kernel arguments in device-tree from user space to load rootfs from differrent partition.

Thanks and best regards.

 
 
Untitle111d.png
0 Kudos
Highlighted
Explorer
Explorer
1,575 Views
Registered: ‎10-03-2018

Hello @sonminh,

Thanks for your reply.

So you are making a "safe" system upgrade.  Cool!

Do you expect that the boot-loader (Das U-Boot) will ever require update? 

Your solution use a flag in the boot environment to determine which set of partitions to boot from.

WARNING: I read through the page link you sent, I did not digest it in detail.

Possible solutions to consider include:

  1. Establish three variables as part of the ${imagekernel} test in the default_boot startup.
    1. kernel partition
    2. file-system partition
    3. kernel boot parameters
  2. Build into the kernel a test against ${imagekernel} which then dynamically selects the root file system.
    1. I think this is a bad solution.
  3. Build a static root file system into image.ub. 
    1. This is what Xilinx did for the ZCU106 which we are using.
    2. This removes the support logic required to maintain synchronisation between multiple sets of partitions.
    3. This method means that there is very limited space for run-time changes to the root file system, which will live in RAM. 
      1. You may want the root file system as read-only, in this case. 
      2. This means that reboots are not going to fail because of root file system changes.   
    4. This may make your boot time a bit slower, due to cross load from flash to RAM.
    5. This may not be viable, if you have insufficient RAM. 
  4. Store your kernel image in the root file system and have Das U-Boot load the kernel therefrom.
    1. Das U-Boot understands several types of file systems, I don't know which off of the top of my head. 
    2. Easy to deal with, as there is only one partition to deal with. 
  5. Quit...
    1. Not really an option, but my sense of humour strikes once in a while. 

So the problems you have to deal with depend on what is to be done about errors. 

Things to consider regarding errors include:

  1. Error control depends on numbers of units and acceptable cost of error recovery. 
    1. These are update failures which require manual intervention. 
    2. Can you tolerate RMA of failed equipment?
    3. Do you have to send a service technician to fix a failed upgrade? 
  2. Will all fielded units have exactly the same partition pair free?
    1. Some fielded units may be working from the first pair, and some the second. 
    2. Do you want to build multiple deliveries with tailored device trees for each set of partitions?  NO!
  3. Will you ever need to change the boot loader?
  4. How many units will use this? 
  5. What chance of error? 
  6. How will you test? 

My suggestion is that you:

  1. Never update the basic boot loader. 
    1. May want to use a two-stage boot for your system. 
  2. Expect to dynamically manage the boot command arguments.
    1. I do not know how the kernel merges the device tree and boot arguments.  Look this up. 
    2. If you change ${imagekernel}, you can have targeted symbols for other aspects of the boot, bootargs, as well. 
    3. Precomputing as much as possible saves potential for failure. 
  3. Clean unwanted and useless symbols from Das U-Boot's environment. 
    1. You don't need this useless clutter. 
    2. It can be recovered from your source control system or by reading a baseline copy of the Das U-Boot source code. 

There is a potential for huge complexity here.  Keep your solution as simple as you can. 

  1. Once a solution path is selected, you are going to be stuck with it. 

Big companies have solved this problem, and it takes a lot of thought and labour to do this well. 

Lastly, did you see in the code for the Das U-Boot symbols where multi-line code is written?  It looks like this, and you can emulate the style of implicit continuation instead of NUL ('\0') at the end of each line:

"uenvboot=" \
"if run sd_uEnvtxt_existence_test; then " \
"run loadbootenv; " \
"echo Loaded environment from ${bootenv}; " \
"run importbootenv; " \
"fi; " \
"if test -n $uenvcmd; then " \
"echo Running uenvcmd ...; " \
"run uenvcmd; " \
"fi\0" \

I know that I am talking around the problem and throwing out suggestions; a lot of how you approach this problem depends on what your requirements are.  For example, there are companies with Linux based products out there with tens of millions of units; they cannot predict anything about when and how a failure occurrs.  Heck, these companies cannot even expect all devices to be at a similar software level.  Other companies are much smaller, and they are willing to tolerate small (for example 1%) failure rates. 

Also, for your information, I expect that we will walk this problem in about six months, so I appreciate discussing this with you.  My issues also include boot time validation of images, and several "cannot fail" type of requirements. 

Good Luck!

 

Kind Regards,
Peimann, S. M.
----
Toddlers are the Storm-Troopers of the Great God Entropy.
Physics: Not Just a Good Idea, It's THE LAW.
0 Kudos
Highlighted
Adventurer
Adventurer
1,548 Views
Registered: ‎10-02-2018

Hi @peimann,

This is section talk about how to use swupdate to update firmware. It is very useful.

https://sbabic.github.io/swupdate/swupdate.html

I will use this tip to update image.ub on partition mtd2 and mtd5.

And in image.ub include bootarg to kernel know where rootfs  be load from.

I will use swupdate to update image1.ub and rootfs1.jffs2: image1.ub into mtd2 aspect rootfs1.jffs2 into mtd3.

And update image2.ub and rootfs2.jffs2: image2.ub into mtd5 aspect rootfs2.jffs2 into mtd6.

Once finish update, I use fw_setenv to modify above variable ${imagekernel} to determine image.ub and root.jffs2 tobe load.

Finaly, I can use two partition to safe update image.ub and rootfs.jffs2.

Next my step is research how to update BOOT.bin into mtd1 and mtd4 and how to choose partition to load BOOT.bin

Thanks

 

0 Kudos
Highlighted
Explorer
Explorer
1,520 Views
Registered: ‎10-03-2018

Hello @sonminh,

That looks like a good find.  Kudos. 

It seems that you have this well in hand now. 

If you need more help, please give me a shout.

Luck!

Kind Regards,
Peimann, S. M.
----
Toddlers are the Storm-Troopers of the Great God Entropy.
Physics: Not Just a Good Idea, It's THE LAW.
0 Kudos