Can I just ask some specific questions:
Is either of these methods ok? I know the 2nd option is, but I haven't yet sorted out a "DI/EI"
It's hard to say without seeing the rest of your code, but I think that if you:
1) mark the buffer and put_index variables 'volatile' or in some other way implement a compiler level barrier to prevent re-ordering,
2) follow the "only one task/interrupt reads and only one task/interrupt writes" restriction
then I think either works.
Note that the DI() / EI() functions *also* act as compiler barriers. They have to, to keep the compiler from moving "protected" code to outside the critical section.
Saving the index update to the end as suggested by Sauli is also fine. It will also save some unnecessary stores to the volatile variable, which is also nice. Tthis is a good example of the reason you need volatile. If you don't specify serial_transmit_buffer_put_index as volatile, it's very likely that the compiler will optimizer will hoist the update out of the loop, either putting the update before or after the loop. If it goes after the loop that is fine, but if it goes before the loop then the ISR can read invalid data.
Also, all the above commentary is correct: volatile is a *compiler* ordering directive. It forces the assembly/machine code to execute certain operations in the same order as the designated flow of the C code. This is fine on (most?) uniprocessor systems: pretty much every CPU architecture guarantees that a single CPU executes code "as if" each instruction is executed sequentially. In multi-core operations, volatile is basically never useful for inter-thread communication. You need atomic operations that include CPU memory fences to guarantee that other CPU cores see the accesses in the correct order. I am assuming you don't have multi-core systems in mind, but there are multi-core microcontrollers so it is certainly not something everyone can ignore.