Ensure you've assigned a valid component into your
controller field in the inspector before running your game.
If you've looked and you're sure it's there, then you might accidentally have another instance of this
PlayerMovement script somewhere else in your scene. Add a
Debug.Log to print out the name of the object in
Start() to help track down the spare, uninitialized instance.
Also, note that capturing horizontal input in
Update(), saving it to a variable then using it in the next
FixedUpdate() will add at least one unnecessary frame of latency to your input, since
Update() runs after
FixedUpdate() in a frame. Instead, you can read and use your axis input in
FixedUpdate() directly to get the latest info "hot off the presses." ;)
I've seen advice claiming that the latest input isn't available until
Update(), but experimentally this is not true for any version of Unity I've tested. It might be superstition left over from an older version, or an over-application of the rule to not naively handle instantaneous inputs (eg. button down events that happen for only one frame) in
FixedUpdate() since it can miss or double-handle them. This is easy to work around though, so lag is usually unnecessary there too.