-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
USART: support reading and writing 9-bit words, change to 32-bit data register access #299
Conversation
I'm not sure what way is better. This one or that is used in: |
…lementations no longer ambiguous
Thanks for bringing that up. Inspired by that version, I made some changes so that u8/u16 is a type parameter (u8 by default) and the serial traits are no longer implemented in an ambiguous way for the same type. |
On which chip did you test that? NB: There are STM peripherals which do weird things when you're trying to send 8bit with a 16bit or even 32bit write; I'm not sure that is the case for any F4 but it is something we should be careful about. Also F446 is one of the later added oddballs in the F4 family; though it seems to have the same serial peripheral version as older versions from the family according to: https://github.com/STMicroelectronics/STM32_open_pin_data/ |
I have tested the I'm glad that we are being cautious about the methods used to access the data register. We shouldn't change something that works without a good reason. However, if the current approach is not fully correct and only works because of hardware implementation details, that is a good reason to change it. As I will explain, the current approach of using 8-bit accesses to the USART data register is inconsistent with all applicable reference manuals and all non-Rust USART drivers that I have been able to find. This approach is risky and may not always work. HistoryThe history of 8-bit reads and writes on the data register goes back at least to May 2017, with the first commit to stm32f103xx-hal. Note that RM0008, the reference manual for the STM32F103 and related models, says that "the [USART] peripheral registers can be accessed by half-words (16-bit) or words (32-bit)." This approach was clearly intentional, and there may have been a good reason for it. I would be happy to learn what the reason is, but I have not been able to find it yet. Other peripheralsSome peripheral registers can be accessed using 8-bit reads and writes. For example, RM0090, the reference manual for several STM32F4 models, allows some 8-bit accesses:
Other peripherals allow only 32-bit register access:
These restrictions tell us that 8-bit accesses to peripheral registers are not universally allowed. When in doubt, we should assume that a register should only be accessed with an instruction that matches its size (for example, 16-bit reads and writes on a 16-bit register). In the USART case, there is no doubt: every STM32F4 reference manual except the STM32F446 clearly states that "The [USART] peripheral registers have to be accessed by half-words (16 bits) or words (32 bits)." The STM32F446 manual is even more restrictive, allowing 32-bit access only. Other driversSTMicroelectronics C LLThe "LL" C drivers provided by STMicroelectronics define the USART registers: typedef struct
{
__IO uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
__IO uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
__IO uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
__IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
__IO uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
__IO uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
__IO uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef; Here are its receive and transmit functions: __STATIC_INLINE uint8_t LL_USART_ReceiveData8(USART_TypeDef *USARTx)
{
return (uint8_t)(READ_BIT(USARTx->DR, USART_DR_DR));
}
__STATIC_INLINE void LL_USART_TransmitData8(USART_TypeDef *USARTx, uint8_t Value)
{
USARTx->DR = Value;
} The data register STMicroelectronics C HALThe "HAL" C drivers provided by STMicroelectronics use the same
Again, they use 32-bit accesses to the data register without any pointer casts. RT-ThreadThis slightly different USART driver uses the same
libopencm3The libopencm3 USART drivers use the #define USART_DR(usart_base) MMIO32((usart_base) + 0x04)
#define MMIO32(addr) (*(volatile uint32_t *)(addr)) This clearly compiles down to 32-bit reads and writes. The current approach works (sometimes)It is clear that 8-bit accesses to the USART data registers do work in the cases that people have tested so far. The problem is that this approach is not guaranteed to work. Any new STM32F4 model, or a new revision of a current model, might not work correctly. There may even be a USART peripheral on some revision of some current STM32F4 model where our existing approach corrupts data or causes a bus error. We do not know about that case because we have not exhaustively tested every possible configuration. We are essentially relying on undefined behavior of hardware, and placing ourselves at the mercy of STMicroelectronics and the tools they use to design their products. ConclusionThe current approach of 8-bit accesses to the USART data registers has a long history but no clear explanation for how it came to be. The reference manuals clearly state that this approach is not supported. No other major driver code accesses the data register this way. The people who use this library are counting on us to provide code that works. Many of us are contributing in our free time with limited access to hardware, so we don't always get everything right. However, we should at least follow the example of other working drivers and work within the restrictions laid out in the reference manuals. What this change means in the compiled codeloop {
nb::block!(tx.write(0x99)).unwrap();
} The above code (following the normal USART initialization) compiles to: Previous
Changed
The only difference is that the data register write instruction is a |
Looks good to me.
Add this to changelog |
bors r+ |
👎 Rejected by too few approved reviews |
bors retry |
@samcrow No worries! Thanks for the elaborate description and research; for me a simple "I've tested this with a few different boards and a few others like ... are doing it the same way" would have sufficed. 😅 |
Thanks; my comment was a little more argumentative than necessary. Now I know more about what kind of justification we expect for changes. |
Motivation
In issue 280, Maldus512 asked how to use 9-bit words with the serial driver. I noticed that the current code did not allow that use, so I decided to improve the code.
Part 1: 16-bit reads and writes for 9-bit words
I added a
WORD
type parameter toSerial
,Tx
, andRx
. It defaults tou8
, so that existing code will not break. I also added functions to convert betweenSerial<USART, PINS, u8>
andSerial<USART, PINS, u16>
.I added some trait implementations that use
u16
as the word type instead ofu8
:impl<USART> serial::Read<u16> for Rx<USART, u16>
impl<USART> serial::Write<u16> for Tx<USART, u16>
impl<USART> blocking::serial::Write<u16> for Tx<USART, u16>
impl<USART, PINS> serial::Read<u16> for Serial<USART, PINS, u16>
impl<USART, PINS> serial::Write<u16> for Serial<USART, PINS, u16>
impl<USART, PINS> blocking::serial::Write<u16> for Serial<USART, PINS, u16>
I also added a simple example that demonstrates that the hardware can send and receive 9 bits.
Probably not a breaking change
Because the
WORD
type parameter has a default value ofu8
, existing code that does not specifyWORD
should continue to compile and work correctly. WhenWORD
is not specified,Serial
,Tx
, andRx
implement the same traits as before.Limitations
It is possible to configure a USART with 8-bit word length and then send/receive
u16
s. We could prevent this error, but it would require a major breaking change to the configuration code that is probably not worth it.DMA with 16-bit words is not yet supported.
Part 2: 32-bit read and write operations on USART_DR
While working on part 1, I noticed that the current code goes out of its way to read or write only 8 bits when accessing the USART data register. Based on the reference manuals, this might be incorrect.
In most STM32F4 reference manuals, the beginning of the "USART registers" section says that "The peripheral registers have to be accessed by half-words (16 bits) or words (32 bits)."
The STM32F446 reference manual is more restrictive: "The peripheral registers have to be accessed by words (32 bits)."
The C drivers provided by ST appear to use 32-bit reads and writes on the data register.
Therefore, I have replaced the custom 8-bit reads and writes with 32-bit reads and writes that use the normal svd2rust API.
With this change applied, the
serial
example still works.