Mastering Software i2c: A Detailed Guide for Embedded Enthusiasts
Introduction
Hey there, embedded system enthusiasts! Today, we’re diving into the world of Software i2c—a nifty way to communicate between your microcontroller and peripheral devices without needing dedicated hardware i2c support. Whether you’re new to embedded systems or a seasoned developer, this guide will walk you through the ins and outs of setting up and using Software i2c.
Why Software i2c?
Sometimes, your microcontroller’s hardware i2c is already in use, or perhaps your MCU doesn’t have i2c hardware support. That’s where Software i2c steps in, giving you the flexibility to implement i2c communication using regular GPIO pins. It’s like giving your MCU a superpower it didn’t know it had!
Let’s Get Our Hands Dirty
We’re going to explore a Software i2c library implementation that works seamlessly with FreeRTOS. The code handles i2c initialization, reading, and writing, making it a robust solution for your next project.
The Core of Our Software i2c Library
Here’s a sneak peek into the i2c.c
file, which contains all the essential functions for our Software i2c library:
/**
* \file i2c.c
* \brief Software i2c library
*/
/*
* Copyright (c) 2024 Jasper
*
* This file contrains all the software i2c functions.
*
* Author: Jasper <JasperZhangSE@gmail.com>
* Version: v1.0.0
*/
#include <cmsis_os.h>
#include "Debug/Debug.h"
#include "FreeRTOS.h"
#include "i2c/i2c.h"
#include "Include/Include.h"
/* Debug config */
#if I2C_DEBUG
#undef TRACE
#define TRACE(...) DebugPrintf(__VA_ARGS__)
#else
#undef TRACE
#define TRACE(...)
#endif /* I2C_DEBUG */
#if I2C_ASSERT
#undef ASSERT
#define ASSERT(a) \
while (!(a)) { \
DebugPrintf("ASSERT failed: %s %d\n", __FILE__, __LINE__); \
}
#else
#undef ASSERT
#define ASSERT(...)
#endif /* I2C_ASSERT */
/* Local defines */
#if SW_I2C_RTOS
#define SW_I2C_MUTEX_INIT() \
do { \
osMutexDef(I2CMutex); \
s_xI2CMutex = osMutexCreate(osMutex(I2CMutex)); \
} while (0)
#define SW_I2C_LOCK() osMutexWait(s_xI2CMutex, osWaitForever)
#define SW_I2C_UNLOCK() osMutexRelease(s_xI2CMutex)
#else
#define SW_I2C_MUTEX_INIT()
#define SW_I2C_LOCK()
#define SW_I2C_UNLOCK()
#endif /* SW_I2C_RTOS */
#if SW_I2C_RTOS
static osMutexId s_xI2CMutex;
#endif
#if SORTWARE_I2C_ENABLE
static void i2c_delay(void) {
#if USE_SYS_TICK_DELAY_US
delay_us(45);
#else
// for (volatile uint32_t i = 0; i < 3000; i++); // 根据系统频率调整
osDelay(1);
#endif
}
Status_t i2c_init(void) {
SW_I2C_MUTEX_INIT();
return STATUS_OK;
}
Status_t i2c_start(void) {
/* __________
*SCL \________
* ______
*SDA \_____________
*/
i2c_SDA(high);
i2c_SCL(high);
i2c_delay();
if (I2C_SDA_READ() == low) {
return STATUS_ERR; /* 检查总线忙 */
}
i2c_SDA(low);
i2c_delay();
i2c_SCL(low);
i2c_delay();
return STATUS_OK;
}
Status_t i2c_stop(void) {
/* ____________
*SCL _____/
* _______
*SDA __________/
*/
i2c_SCL(low);
i2c_delay();
i2c_SDA(low);
i2c_delay();
i2c_SCL(high);
i2c_delay();
i2c_SDA(high);
i2c_delay();
return STATUS_OK;
}
Status_t i2c_write_byte(uint8_t byte) {
TRACE("i2c write byte = ");
for (uint8_t i = 0; i < 8; i++) {
if (byte & 0x80) {
i2c_SDA(high);
}
else {
i2c_SDA(low);
}
TRACE("%d ", (byte & 0x80) >> 7);
i2c_delay();
i2c_SCL(high);
i2c_delay();
i2c_SCL(low);
i2c_delay();
byte <<= 1;
}
TRACE("\r\n");
/* 等待 ACK */
i2c_SDA(high);
i2c_delay();
i2c_SCL(high);
i2c_delay();
if (I2C_SDA_READ() == high) {
TRACE("i2c_write_byte error\r\n");
return STATUS_ERR;
}
i2c_SCL(low);
return STATUS_OK;
}
Status_t i2c_read_byte(uint8_t *byte, uint8_t ack) {
/* 检查空指针 */
if (!byte) {
return STATUS_ERR;
}
TRACE("i2c read byte = ");
*byte = 0;
i2c_delay();
for (uint8_t i = 0; i < 8; i++) {
*byte <<= 1;
i2c_SCL(high);
i2c_delay();
if (I2C_SDA_READ() == high) {
*byte |= 0x01;
}
TRACE("%d ", (*byte & 0x01));
i2c_SCL(low);
i2c_delay();
}
TRACE("\n");
/* 发送 ACK 或 NACK */
if (ack) {
i2c_SDA(high);
}
else {
i2c_SDA(low);
}
i2c_delay();
i2c_SCL(high);
i2c_delay();
i2c_SCL(low);
i2c_delay();
i2c_SDA(high);
return STATUS_OK;
}
Status_t i2c_write_data(uint8_t device_address, uint8_t *data, uint16_t length) {
/* 检查空指针 */
if (!data) {
return STATUS_ERR;
}
/* 发送启动信号 */
if (i2c_start() != STATUS_OK) {
return STATUS_ERR;
}
/* 发送设备地址(写模式) */
if (i2c_write_byte(device_address << 1 & 0xFE) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
/* 发送数据 */
for (uint16_t i = 0; i < length; i++) {
if (i2c_write_byte(data[i]) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
TRACE("data[%d] = %d\r\n", i, data[i]);
}
TRACE("\r\n");
/* 发送停止信号 */
i2c_stop();
return STATUS_OK;
}
Status_t i2c_read_data(uint8_t device_address, uint8_t *data, uint16_t length, uint8_t ack) {
/* 检查空指针 */
if (!data) {
TRACE("data is NULL\r\n");
return STATUS_ERR;
}
/* 发送启动信号 */
if (i2c_start() != STATUS_OK) {
TRACE("start error\r\n");
return STATUS_ERR;
}
/* 发送设备地址(读模式) */
if (i2c_write_byte(device_address << 1 | 0x01) != STATUS_OK) {
TRACE("write address error\r\n");
i2c_stop();
return STATUS_ERR;
}
/* 接收数据 */
for (uint16_t i = 0; i < length; i++) {
if (i2c_read_byte(&data[i], (i < length - 1) ? ack : 1) != STATUS_OK) {
TRACE("i2c_read_data error\r\n");
i2c_stop();
return STATUS_ERR;
}
TRACE("data[%d] = %d\r\n", i, data[i]);
}
TRACE("\r\n");
/* 发送停止信号 */
i2c_stop();
return STATUS_OK;
}
Status_t i2c_write_register(uint8_t device_address, uint8_t register_address, uint8_t *data, uint8_t length) {
/* 检查空指针 */
if (!data) {
return STATUS_ERR;
}
SW_i2c_LOCK();
/* 发送启动信号 */
if (i2c_start() != STATUS_OK) {
return STATUS_ERR;
}
/* 发送设备地址(写模式) */
if (i2c_write_byte(device_address << 1 & 0xFE) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
/* 发送寄存器地址 */
if (i2c_write_byte(register_address) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
/* 发送数据 */
for (uint8_t i = 0; i < length; i++) {
/* 使用数组下标访问数据 */
if (i2c_write_byte(data[i]) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
}
/* 发送停止信号 */
i2c_stop();
SW_i2c_UNLOCK();
return STATUS_OK;
}
Status_t i2c_read_register(uint8_t device_address, uint8_t register_address, uint8_t *data, uint8_t length) {
/* 检查空指针 */
if (!data) {
return STATUS_ERR;
}
SW_i2c_LOCK();
/* 发送启动信号 */
if (i2c_start() != STATUS_OK) {
return STATUS_ERR;
}
/* 发送设备地址和写位(左移一位并将最低位清零) */
if (i2c_write_byte((device_address << 1) & 0xFE) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
/* 发送寄存器地址 */
if (i2c_write_byte(register_address) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
/* 发送起始条件(重复开始) */
if (i2c_start() != STATUS_OK) {
return STATUS_ERR;
}
/* 发送设备地址和读位 (左移一位并将最低位置 1) */
if (i2c_write_byte((device_address << 1) | 0x01) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
for (uint8_t i = 0; i < length; i++) {
if (i == length - 1) {
// 最后一个字节,发送NACK // 第二个参数1表示NACK
if (i2c_read_byte(&data[i], 1) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
}
else {
// 不是最后一个字节,发送ACK// 第二个参数0表示ACK
if (i2c_read_byte(&data[i], 0) != STATUS_OK) {
i2c_stop();
return STATUS_ERR;
}
}
}
/* 发送停止条件 */
i2c_stop();
SW_i2c_UNLOCK();
return STATUS_OK;
}
Key Functions Explained
Let’s break down the key functions in our Software i2c library:
Initialization: i2c_init
The i2c_init
function initializes the mutex if we’re using FreeRTOS to manage concurrency. It’s crucial to ensure that multiple tasks don’t mess up the i2c communication by accessing it simultaneously.
Starting Communication: i2c_start
This function generates the start condition required to initiate i2c communication. It pulls the SDA line low while keeping the SCL line high, signaling the start of a transmission.
Stopping Communication: i2c_stop
The i2c_stop
function generates the stop condition, ending the communication. It releases the SDA line after pulling the SCL line high, which tells the i2c devices that the communication has ended.
Writing a Byte: i2c_write_byte
This function sends a byte of data over the i2c bus. It shifts each bit of the byte to the SDA line and toggles the SCL line to clock the data out. After sending all bits, it waits for an ACK from the slave device.
Reading a Byte: i2c_read_byte
The i2c_read_byte
function reads a byte of data from the i2c bus. It shifts bits into a byte while toggling the SCL line. After receiving the byte, it sends an ACK or NACK depending on the ack
parameter.
Advanced Functions
Apart from the basic read and write operations, our library also supports reading and writing multiple bytes and registers. This makes it versatile for various i2c devices, from simple sensors to complex peripherals.
Writing Multiple Bytes: i2c_write_data
This function sends multiple bytes of data to a specified device address. It starts with the device address in write mode, followed by the data bytes.
Reading Multiple Bytes: i2c_read_data
The i2c_read_data
function reads multiple bytes of data from a specified device address. It sends the device address in read mode and receives the specified number of bytes.
Writing to Registers: i2c_write_register
This function writes data to a specific register of a device. It’s handy for devices like sensors where you need to configure settings before starting measurements.
Reading from Registers: i2c_read_register
Similar to i2c_write_register
, this function reads data from a specific register. It’s useful for fetching sensor readings or status bytes from peripherals.
Wrapping Up
Software i2c is a powerful tool in your embedded systems arsenal, giving you the flexibility to implement i2c communication without hardware constraints. With the provided library and detailed explanation, you’re well-equipped to add i2c communication to your next project.
Got any questions or need further clarification? Drop a comment below or reach out via email. Happy coding!
Feel free to adapt and expand this guide according to your specific needs and preferences. Let me know if you have any other questions or need further assistance!