You can for ARM like I've described with something like the BeagleBoard. For a specific ARM processor, it's a matter of searching for a distribution or trying one that doesn't require MMU on the processor you've chosen. I'd recommend first using something that someone has already resolved so that you can understand how the kernel was configured and the HAL was done. Also important is the boot loader where the chip and environment are set up in advance for the kernel to load and utilize that specific processor's resources.
PIC, no. If there is one I suspect it's very limited and not really Linux with most of the capabilities, instead just that someone got it working to a degree so that they could say, "I did it!".
I'd recommend two things.
(1) Program for real in Linux on a current full blown machine, running Linux. You can attach all kinds of stuff via the USB or serial ports, or USB-Serial. You can attach a camera, a game controller, an IR detector; just something which can give you input via USB or serial where you can write a program to attach to that port and grab the data. Here what you want to learn are two things (a) how to interface with Linux resources (b) how to architect under the Linux system services.
I'm not sure there's any specific guide unless someone reading this knows of good references. Probably opinion based too.
My example is that I program a lot in Linux utilizing numerous serial devices. The architectures we use are Linux core computer running a GUI which is driven by one of the various vendors; such as Qt, under which there are various sensors performing real time data collection, but sending either raw or filtered data via serial connections to my Linux core. Linux processes grab that data and communicate via pipe(2). Since there are some cross communications; such as the top GUI needing to send commands down to the other processes, and also those processes always sending data upwards to the GUI, I've developed architectures using fork(2) and execvp(2) so that the pipes are known to each other. Then I use select(2) and recv(2) to determine if there is new data and process it accordingly.
That's a little disorganized, sorry. Summary is: On boot up, I create a daemon, here's a good example.
Of course I extend that because things like exiting or other things are not conducive to an embedded system; i.e. we have speakers, LEDs etc to indicate critical failure, or we retry and retry indicating we are in a phase of booting until we can either succeed or the user has to attach a serial terminal and debug the problem.
Next from that daemon I create common resources, for instance my pipe(2) mechanisms for communications to/from each process I plan to use. I've developed a function to fork() and exec() my child processes. And because the daemon created these processes, it has their pid's and therefore can wait on signals from them and determine if they were killed or exited in error. And then the daemon can then re-spawn a failed process or indicate critical error. But that's all the daemon does, gets it going and then monitors and either attempts recovery or indicates failure. Remember that this is for an embedded system, not really a keyboard or monitor, but instead lights and limited push buttons; like a Coke machine or something, right?
The next step is that each process does something specific; for instance one talks to accelerometers, another talks to an environmental sensor, another one talks to a metal detector maybe. So to extend the Coke machine thought example, acceleration is whether or not some idiot is trying to bang the machine over and maybe that process indicates danger to the idiot that a thousand pound machine may fall on top of them. The environmental sensor detects whether or not refrigeration is working properly, and metal detector detects coins inserted. Each process may inject information to the GUI to tell the user what's happening. I.e. You inserted 5 cents, I got it, but that's not enough yet to make a selection; touch REFUND or insert more coins ... That information was conveyed via one process sending to another, which I use pipes for; however there are a variety of IPC mechanisms in Linux, see:
And you can choose how to communicate between two processes in a manner which best suits you.
You may wish to read that whole reference, it talks a lot about Linux in general:
Further, because it's embedded there may be a serial port or some way for the administrator/owners to communicate; which isn't available to a normal user. We keep logs on the system so when someone attaches via networking or a terminal, they'll be able to obtain those logs and diagnose.
The second recommendation is to then explore customizing distributions where you cut down the kernel and services that are included in the boot so that you have a minimal version; running what you want, but not running extras that are not needed. For instance, if you're an embedded device with a custom display, you may not need an XServer at all, you may not need CUPS (printer driver), you may not even need any network stacks if you are solely communicating via serial to a device, although most things usually use networking or wireless to interface these days.
So if coming up to speed more on how you can use Linux user programming to accomplish things like talking to peripherals and such, there are some thoughts. I'm not really a kernel person, I do what I need to get things accomplished on that front and the most I've done is customized an existing driver to add more features to it. But reading up on Linux drivers is good too: