Summary
The bare-metal I2C3 initialization and transmit sequence fails because the timing register is invalid (0x00000000), the slave address handling is incorrect for a 7-bit write operation, and peripheral clock configuration is incomplete. The HAL driver handles peripheral reset, clock setup, and timing calculation correctly, while the bare-metal code attempts to start transactions without a functional I2C configuration.
Root Cause
The failure stems from three critical errors in the bare-metal implementation:
- Invalid I2C Timing: The
I2C3->TIMINGRregister is set to0x00000000. This configuration corresponds to a maximum bus speed (effectively DC) and does not meet the standard-mode 100 kHz requirement. Consequently, the peripheral cannot generate valid clock signals or sample data correctly. - Incorrect Address and Direction: The
CR2register is configured with(0x23 << 1). While0x23is the device address, shifting it left by 1 bit implies a write to a 7-bit slave address. However, the standardCR2expects the 7-bit address already shifted by one bit (or the 8-bit address for 10-bit mode). Furthermore, theRWbit (direction) is missing. - Missing Peripheral Reset: The bare-metal code enables the I2C3 clock (
RCC->APB1ENR1) but does not perform the mandatory hardware reset (RCC->APB1RSTR1) and release sequence. Without a reset, residual configurations from the bootloader or previous state remain, potentially blocking the bus.
Why This Happens in Real Systems
I2C is a synchronous protocol with strict timing constraints. Unlike UART, which can often tolerate some timing mismatch, I2C relies on precise rise/fall times and setup/hold windows.
- Timing Registers: STM32 hardware I2C requires a specific
PRESCALER,SCLDEL,SDADEL,SCLH, andSCLLto generate the 100 kHz waveform. A value of0is physically invalid. - State Machine: The I2C peripheral is a state machine. If the
CR1orCR2is not set to the required states (e.g., addressing mode, NBYTES, STOP/START generation), the peripheral will not initiate theSTARTcondition, or it will stall indefinitely waiting for events that never trigger.
Real-World Impact
- Bus Hang: An invalid configuration can leave the SDA and SCL lines in a stuck state, requiring a hardware power cycle to recover.
- Silent Failure: The MCU registers may appear to accept writes, but the external device (PCA9535) never receives the data, resulting in zero functionality without obvious error flags.
- Latency Waste: Polling loops (e.g.,
while(!(I2C3->ISR & I2C_ISR_TXIS))) will spin forever, blocking the main execution thread.
Example or Code
Below is the corrected bare-metal initialization sequence. It includes the necessary peripheral reset, valid timing calculation for 100 kHz (assuming 16MHz APB1 clock), and correct address formatting.
#include "stm32g4xx.h"
// Function to calculate I2C timing for 100kHz with 16MHz PCLK
// Standard Mode: t_SCLL/t_SCLH = 250ns min, t_SYNC1/2 = 200ns min
// With PRESC=0, t_PCLK = 62.5ns. We need ~15.6 cycles per half-period.
void I2C3_Init_BareMetal(void) {
// 1. Enable and Reset Peripheral
RCC->APB1ENR1 |= RCC_APB1ENR1_I2C3EN;
RCC->APB1RSTR1 |= RCC_APB1RSTR1_I2C3RST;
RCC->APB1RSTR1 &= ~RCC_APB1RSTR1_I2C3RST;
// 2. Configure GPIO (PC8/SCL, PC9/SDA)
// Reset GPIOC
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOCEN;
// Configure PC8 and PC9 as Alternate Function Open-Drain
// Mode: 10 (Alternate Function)
GPIOC->MODER &= ~((3U << 16) | (3U <MODER |= ((2U << 16) | (2U <OTYPER |= ((1U << 8) | (1U <AFR[1] &= ~((0xFU << 0) | (0xFU <AFR[1] |= ((8U << 0) | (8U <TIMINGR = (3 << 28) | (3 << 20) | (1 << 16) | (15 << 8) | (21 <CR1 |= I2C_CR1_PE;
}
void I2C3_WriteRegister(uint8_t slaveAddr7bit, uint8_t reg, uint8_t data) {
// Wait for bus to be free
while (I2C3->ISR & I2C_ISR_BUSY);
// Configure CR2 for the transaction
// 7-bit addressing: Address is left-shifted by 1 (bit 0 is R/W)
// 0x23 <CR2 = (slaveAddr7bit << 1) | (2 <ISR & I2C_ISR_TXIS));
I2C3->TXDR = reg;
while (!(I2C3->ISR & I2C_ISR_TXIS));
I2C3->TXDR = data;
// Wait for STOP flag (AutoEnd generates it)
while (!(I2C3->ISR & I2C_ISR_STOPF));
I2C3->ICR |= I2C_ICR_STOPCF; // Clear flag
}
int main(void) {
// Minimal System Init (Assuming default MSI/HSI clock 16MHz)
// In production, ensure PLL is configured correctly.
I2C3_Init_BareMetal();
// PCA9535 Address: 0x23 (A2=0, A1=1, A0=1)
// 7-bit address is 0x23. Left shift for write: 0x46
// Defined as macro for clarity
#define PCA_ADDR_WRITE (0x23 << 1)
// Set all ports as OUTPUT
I2C3_WriteRegister(PCA_ADDR_WRITE, 0x06, 0x00); // Config 0
I2C3_WriteRegister(PCA_ADDR_WRITE, 0x07, 0x00); // Config 1
// Enable output ports
I2C3_WriteRegister(PCA_ADDR_WRITE, 0x02, 0xAA); // Output 0
I2C3_WriteRegister(PCA_ADDR_WRITE, 0x03, 0x01); // Output 1
while (1) {}
}
How Senior Engineers Fix It
Senior engineers approach bare-metal I2C by validating the physical layer before logical transmission:
- Reference Manual Calculation: They do not guess timing values. They calculate
TIMINGRbased on the MCU clock tree and I2C spec (tSU, tHD, tLOW, tHIGH). Using a logic analyzer to verify the SCL waveform is the definitive test. - Peripheral Lifecycle Management: They ensure a full Reset -> Enable sequence. This clears any stuck bits in the peripheral from previous runs or bootloader states.
- Address Standardization: They explicitly handle the LSB of the address byte. For a 7-bit write, the address must be
(SlaveAddress << 1). Using the raw address (without shift) usually results in a Read attempt or a NACK. - State Machine Awareness: They understand that
CR2must be written beforeTXDRis filled and thatSTARTbit triggers the hardware state machine. They rely onAUTOENDto handle STOP generation cleanly.
Why Juniors Miss It
- Copy-Paste Errors: Juniors often copy register addresses or values without understanding the underlying arithmetic. For example, copying
0x23directly intoCR2without realizing the register expects a shifted value. - Overlooking Default States: Assuming that enabling a clock (
APBxENR) initializes the peripheral. In reality, peripherals often start in undefined or “frozen” states requiring an explicit reset. - Lack of Timing Calculation: Trying to use “default” timing values found in online forums rather than calculating them for the specific clock source (e.g., HSI vs HSE).
- Debugging Blindly: Checking register values in the debugger is helpful, but without a logic analyzer, it’s impossible to know if the SCL line is actually toggling or if the signal integrity is poor due to missing open-drain configuration.