Basic Puya PY32F003 I2C peripheral code¶
I always find it useful to get a basic external interface working on embedded projects as early as possible. This enables visibility and configurability of a running system during the development process. I2C is often the interface of choice.
Chinese STM32F030-style chips like the PY32F003 are often considered clones, but at least the I2C peripheral of the PY32F003 seems quite a bit different from the one in the STM32F030. Unfortunately at the time of writing the reference manual still only exists in Chinese, so using the I2C peripheral effectively took a bit more work than hoped. The provided driver code was actually the most useful source of information.
The following code snippet shows how I initialized the I2C peripheral
to work on pins PF1
and PF0
:
/* System clock frequency in run mode */
#define SYS_CLK_FREQ 24000000UL
/* I2C clock frequency */
#define I2C_CLK_FREQ 400000UL
/* I2C slave address */
#define I2C_ADDRESS 0x43
/* I2C config */
static void I2CConfig(void)
{
/* Turn on the clock for the GPIO block F */
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOF);
/* Configure PF0 and PF1 for I2C bus (PF0 = SDA, PF1 = SCL) */
LL_GPIO_SetPinMode(GPIOF, LL_GPIO_PIN_0, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinMode(GPIOF, LL_GPIO_PIN_1, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetAFPin_0_7(GPIOF, LL_GPIO_PIN_0, LL_GPIO_AF_12);
LL_GPIO_SetAFPin_0_7(GPIOF, LL_GPIO_PIN_1, LL_GPIO_AF_12);
/* Turn on the I2C clock */
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);
/* Disable the I2C peripheral */
LL_I2C_Disable(I2C1);
/* Configure I2C speed */
LL_I2C_ConfigSpeed(I2C1, SYS_CLK_FREQ, I2C_CLK_FREQ, LL_I2C_DUTYCYCLE_2);
/* Configure I2C slave address (shift left because this macro expects an
* 8-bit address, and our define is a 7-bit address) */
LL_I2C_SetOwnAddress1(I2C1, I2C_ADDRESS << 1, 0);
/* Enable interrupts */
SET_BIT(I2C1->CR2, I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN);
NVIC_EnableIRQ(I2C1_IRQn);
/* Enable clock stretching */
LL_I2C_EnableClockStretching(I2C1);
/* Enable the selected I2Cx Peripheral */
LL_I2C_Enable(I2C1);
/* Start out ACKnowledging incoming requests */
LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK);
}
This configuration seemed to work with both a standard 100 kHz clock and in "fast" 400 kHz clock mode.
For the I2C register set to implement, I usually like to start out with a simple memory area so I can implement and test reading and writing. In this case, the following code snippet implements a simple 32-byte RAM area to read from and write to, using interrupt driven access:
/* I2C data structure */
#define I2C_DATA_LEN (32)
struct sI2CData
{
uint32_t idx;
bool set_idx;
bool last_op_read;
uint8_t data[I2C_DATA_LEN];
} static volatile i2c;
/* I2C interrupt handler */
void I2C1_IRQHandler(void)
{
/* A master is addressing us? */
if (LL_I2C_IsActiveFlag_ADDR(I2C1))
{
/* Clear the flag */
LL_I2C_ClearFlag_ADDR(I2C1);
/* Is the master writing and we are reading? */
if (LL_I2C_GetTransferDirection(I2C1) == LL_I2C_DIRECTION_READ)
{
/* Then prepare to receive the index first */
i2c.set_idx = true;
}
/* Master is reading, if the last operation was a read, our index will
* be one count too high now and should be backed up */
else if (i2c.last_op_read)
{
i2c.idx--;
}
}
/* Stop condition flag set */
else if (LL_I2C_IsActiveFlag_STOP(I2C1))
{
/* Clear the flag */
LL_I2C_ClearFlag_STOP(I2C1);
/* ACK the next byte addressed to us */
LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK);
}
/* TX empty flag set */
else if (LL_I2C_IsActiveFlag_TXE(I2C1))
{
/* Are we still within our valid data? */
if (i2c.idx < I2C_DATA_LEN)
{
/* Read data from buffer, increment index */
LL_I2C_TransmitData8(I2C1, i2c.data[i2c.idx++]);
/* Indicate the last operation was a read */
i2c.last_op_read = true;
}
else
{
/* Read 0xFF outside valid range */
LL_I2C_TransmitData8(I2C1, 0xFF);
/* Indicate the last operation was not a read from buffer */
i2c.last_op_read = false;
}
}
/* RX not empty flag set */
else if (LL_I2C_IsActiveFlag_RXNE(I2C1))
{
/* Are we receiving the index? */
if (i2c.set_idx)
{
/* Set the index */
i2c.idx = LL_I2C_ReceiveData8(I2C1);
/* Done setting the index */
i2c.set_idx = false;
}
else
{
/* Can we write the received data at the current index? */
if (i2c.idx < I2C_DATA_LEN)
{
/* Then write the received data */
i2c.data[i2c.idx++] = LL_I2C_ReceiveData8(I2C1);
}
else
{
/* Can't write the received data, just read to clear the flag */
__IO uint32_t tmpreg;
tmpreg = LL_I2C_ReceiveData8(I2C1);
(void)tmpreg;
}
}
/* Is the index beyond our buffer size? */
if (i2c.idx >= I2C_DATA_LEN)
{
/* Then we should send NACK if we receive another byte
* Good idea, but it doesn't seem to work? */
LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_NACK);
}
/* Indicate the last operation was not a read */
i2c.last_op_read = false;
}
}
A useful register implementation may be created by making i2c.data
a union
that contains a struct
naming the registers and a byte array in the same
memory space that the I2C peripheral can access. This works for simple cases
where you only have 8-bit registers and writing a register does not need to
trigger any side effects like updating hardware.
A more thorough implementation would likely use function calls to read and write data so things like caching of multi-byte registers, and side effects on read and write can be implemented. Make sure to keep these functions short and fast since they run in the interrupt context!
There are some weird things in the above code, it's not as clean as I'd like but I was getting some weird corner cases that needed to be handled, such as requiring to move the index back for repeated read accesses, and the NACK for writing outside the valid range not working. It may be that the lack of English documentation caused me to implement something wrong, and I may revisit this in the future once better documentation becomes available. But for now, this worked for me. So I'll build on this to implement a more full-featured implementation that caches multi-byte registers and can trigger side effects.