概述
在上一篇《普冉(PUYA)单片机开发笔记(10): I2C通信-配置从机-CSDN博客》配置了 PY32F003 的 I2C 从机一侧,今天配置主机,并实现主-从机之间的报文收发。
为了完成这个实验,需要两块 PY32F003F18P 的开发板,连接线路如下表所示。注意主从机必须共地,要专门拉一根杜邦线把两块开发板的 GND 连接好。
开发板 | 主机 | 从机 | 备注 |
3V3 | 连接 | 连接 | 同一个电源 |
GND | 连接 | 连接 | 主从机必须共地 |
UART2_TX | PA1 | PA1 | 接到上位机 USB-TTL 模块 |
UART2_RX | PA0 | PA0 | 接到上位机 USB-TTL 模块 |
I2C_SCL | PA3 | PA3 | 主从用杜邦线连接 |
I2C_SDA | PA2 | PA2 | 主从用杜邦线连接 |
连接好以后,在上位机开两个 XCOM 的窗口,各自连接主机和从机的串口,以便观察运行结果。就像附后“实验结果”中的截图,上位机的 COM3 接从机的串口,COM32 接主机的串口。
代码实现步骤
老套路,main.h 中定义函数原型,app_i2c.c 中实现这些函数,py32_f0xx_hal_msp.c 中对I2C 用到的管脚进行初始化,对用到的中断和 DMA 进行初始化;py32_f0xx_it.c 中重定向 I2C_IRQHandler。上干货 ;)
1. 在 main.h 中定义四个函数原型
#define I2C_COMM_TEST 1
#define I2C_SLAVE 0
...
...
#if(I2C_COMM_TEST)
/** ----------------------------------------------------------------------------
* @name : HAL_StatusTypeDef app_i2c_init(void);
* @brief : i2c 初始化
* @param :
* @retval : HAL_OK: 写入成功; 其它: 错误
* @remark : 上级函数必须检查操作返回值, 只有 HAL_OK 时才可继续操作
*** ----------------------------------------------------------------------------
*/
HAL_StatusTypeDef app_i2c_init(void);
/** ----------------------------------------------------------------------------
* @brief : i2c 测试使用的三个函数, 接收/发送/等待
* @param :
* @retval :
* @remark :
*** ----------------------------------------------------------------------------
*/
#if(I2C_SLAVE)
HAL_StatusTypeDef app_i2c_slave_receive(void);
HAL_StatusTypeDef app_i2c_slave_transmit(void);
#else
HAL_StatusTypeDef app_i2c_master_receive(void);
HAL_StatusTypeDef app_i2c_master_transmit(void);
#endif
void app_i2c_wait(void);
#endif
其中,为了在同一个工程中容纳主从两部分的代码,预定了 I2C_COMM_TEST 和 I2C_SLAVE,如果 I2C_SLAVE 为 1 表示定义的是从机的函数,如果 I2C_SLAVE 为 0 表示定义的是主机的函数。
在 PY32F003 上,主机和从机的初始化函数都取一样的函数名;动作函数 app_i2c_wait() 函数也使用一样的函数名。需要区分的是 app_i2c_slave/master_tx/rx 函数,因此分主机和从机定义了两组。
2. 在 app_i2c.c 中实现这些代码
/**
******************************************************************************
* @file app_i2c.c
* @brief I2C functions.
******************************************************************************
* @copyright
*
* Copyright (c) 2023 CuteModem Intelligence.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
#include "main.h"
#define EXDATA_LEN 15 // 数据长度
#define I2C_ADDRESS 0xA0 // 本机地址0xA0
#define I2C_SPEEDBPS 100000 // 通讯速度100K
#define I2C_DUTYCYCLE I2C_DUTYCYCLE_16_9 // 占空比
I2C_HandleTypeDef I2cHandle;
#if(I2C_SLAVE)
uint8_t mI2cTxBuf[EXDATA_LEN] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
#else
uint8_t mI2cTxBuf[EXDATA_LEN] = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30};
#endif
uint8_t mI2cRxBuf[EXDATA_LEN] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
HAL_StatusTypeDef app_i2c_init(void)
{
HAL_StatusTypeDef cfg_res = HAL_OK;
I2cHandle.Instance = I2C; // I2C
I2cHandle.Init.ClockSpeed = I2C_SPEEDBPS; // I2C 通讯速度
I2cHandle.Init.DutyCycle = I2C_DUTYCYCLE; // I2C 占空比
I2cHandle.Init.OwnAddress1 = I2C_ADDRESS; // I2C 地址
I2cHandle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; // 禁止广播呼叫
I2cHandle.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 允许时钟延长
cfg_res = HAL_I2C_Init(&I2cHandle); // I2C初始化
if (cfg_res != HAL_OK) return cfg_res;
return cfg_res;
}
void app_i2c_wait(void)
{
/* 判断当前I2C状态, 等待I2C状态就绪 */
while (HAL_I2C_GetState(&I2cHandle) != HAL_I2C_STATE_READY){}
}
#if(I2C_SLAVE)
HAL_StatusTypeDef app_i2c_slave_receive(void)
{
/*I2C从机中断方式接收*/
while (HAL_I2C_Slave_Receive_IT(&I2cHandle, (uint8_t *)mI2cRxBuf, EXDATA_LEN) != HAL_OK)
Error_Handler();
return HAL_OK;
}
HAL_StatusTypeDef app_i2c_slave_transmit(void)
{
/*I2C从机中断方式发送*/
while (HAL_I2C_Slave_Transmit_IT(&I2cHandle, (uint8_t *)mI2cTxBuf, EXDATA_LEN) != HAL_OK)
Error_Handler();
return HAL_OK;
}
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
uint8_t i = 0;
if(hi2c->Instance != I2C) return;
printf("Slave received via I2C: %d bytes\r\n", EXDATA_LEN);
for(i = 0; i < EXDATA_LEN; i++) printf("%02d ", mI2cRxBuf[i]);
printf("\r\n");
return;
}
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
uint8_t i = 0;
if(hi2c->Instance != I2C) return;
printf("Slave data sent via I2C: %d bytes\r\n", EXDATA_LEN);
for(i = 0; i < EXDATA_LEN; i++) printf("%02d ", mI2cTxBuf[i]);
printf("\r\n");
return;
}
#else
HAL_StatusTypeDef app_i2c_master_receive(void)
{
/*I2C 主机以中断方式接收*/
while (HAL_I2C_Master_Receive_IT(&I2cHandle, I2C_ADDRESS, (uint8_t *)mI2cRxBuf, EXDATA_LEN) != HAL_OK)
Error_Handler();
app_i2c_wait();
return HAL_OK;
}
HAL_StatusTypeDef app_i2c_master_transmit(void)
{
/*I2C 主机以中断方式发送*/
while (HAL_I2C_Master_Transmit_IT(&I2cHandle, I2C_ADDRESS, (uint8_t *)mI2cTxBuf, EXDATA_LEN) != HAL_OK)
Error_Handler();
app_i2c_wait();
return HAL_OK;
}
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
uint8_t i = 0;
if(hi2c->Instance != I2C) return;
printf("Master received via I2C: %d bytes\r\n", EXDATA_LEN);
for(i = 0; i < EXDATA_LEN; i++) printf("%02d ", mI2cRxBuf[i]);
printf("\r\n");
return;
}
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
uint8_t i = 0;
if(hi2c->Instance != I2C) return;
printf("Master data sent via I2C: %d bytes\r\n", EXDATA_LEN);
for(i = 0; i < EXDATA_LEN; i++) printf("%02d ", mI2cTxBuf[i]);
printf("\r\n");
return;
}
#endif
有一部分代码已经在从机配置中实现了,做主机时使用 I2C_SLAVE 开关把主从两部分的数据做一个区分。
- 从机的发送缓冲区填充 1~15;主机的发送缓冲区填充 16~30,以示区别。
- app_i2c_init() 函数和 app_i2c_wait() 函数主从机共用。
- 根据 I2C_SLAVE 开关分别实现主/从机的发送(Transmit)、接收(Receive)、发送中断服务回调函数(TxCpltCallback)和接收中断服务回调函数(RxCpltCallback)这四对函数。
- I2C 主从收发函数结构相似,注意 HAL_I2C_Master_Transmit_IT() 函数比 HAL_I2C_Slave_Transmit_IT() 函数多了一个参数:I2C_Address。HAL_I2C_Master_Receive_IT() 函数比 HAL_I2C_Receive_IT() 函数也多了这个参数。这个参数用于区分 I2C 从机,以便在 RxCpltCallback 函数中处理。区分的 I2C 从机的参数在 hi2c 指针中,在 RxCpltCallback 中读取 hi2c->Devaddress 的值可区分数据是来自哪一个从机。
3. py32_f0xx_hal_msp.c 中分配管脚和DMA,并进行初始化
#include "main.h"
static DMA_HandleTypeDef HdmaCh1;
static DMA_HandleTypeDef HdmaCh2;
...
...
#if(I2C_COMM_TEST)
/**
* -----------------------------------------------------------------------
* @name : void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
* @brief : 初始化I2C相关MSP
* @param : [in] *hi2c, I2C handler pointer
* @retval : void
* @remark :
* -----------------------------------------------------------------------
*/
void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_SYSCFG_CLK_ENABLE(); //SYSCFG时钟使能
__HAL_RCC_DMA_CLK_ENABLE(); //DMA时钟使能
__HAL_RCC_I2C_CLK_ENABLE(); //I2C时钟使能
__HAL_RCC_GPIOA_CLK_ENABLE(); //GPIOA时钟使能
/**I2C GPIO Configuration
PA3 : I2C1_SCL
PA2 : I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 推挽方式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_I2C; // 复用为I2C
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // GPIO初始化
__HAL_RCC_I2C_FORCE_RESET(); // 复位I2C
__HAL_RCC_I2C_RELEASE_RESET();
/* I2C1 interrupt Init */
HAL_NVIC_SetPriority(I2C1_IRQn, 0, 0); // 中断优先级设置
HAL_NVIC_EnableIRQ(I2C1_IRQn); // 使能I2C中断
//DMA配置
HAL_SYSCFG_DMA_Req(9); //DMA1_CH1 MAP选择为 IIC_TX
HAL_SYSCFG_DMA_Req(0xA00); //DMA1_CH2 MAP选择为 IIC_RX
/* Configure the DMA handler for Transmission process */
HdmaCh1.Instance = DMA1_Channel1; // 选择DMA通道1
HdmaCh1.Init.Direction = DMA_MEMORY_TO_PERIPH; // 方向为从存储器到外设
HdmaCh1.Init.PeriphInc = DMA_PINC_DISABLE; // 禁止外设地址增量
HdmaCh1.Init.MemInc = DMA_MINC_ENABLE; // 使能存储器地址增量
HdmaCh1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据宽度为8位
HdmaCh1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 存储器数据宽度位8位
HdmaCh1.Init.Mode = DMA_NORMAL; // 禁止循环模式
HdmaCh1.Init.Priority = DMA_PRIORITY_VERY_HIGH; // 通道优先级为"很高"
HAL_DMA_Init(&HdmaCh1); // 初始化DMA通道1
__HAL_LINKDMA(hi2c, hdmatx, HdmaCh1); // DMA1 关联 IIC_TX
/* Configure the DMA handler for Receiving process */
HdmaCh2.Instance = DMA1_Channel2; // 选择DMA通道1
HdmaCh2.Init.Direction = DMA_PERIPH_TO_MEMORY; // 方向为从外设到存储
HdmaCh2.Init.PeriphInc = DMA_PINC_DISABLE; // 禁止外设地址增量
HdmaCh2.Init.MemInc = DMA_MINC_ENABLE; // 使能存储器地址增量
HdmaCh2.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据宽度为8位
HdmaCh2.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 存储器数据宽度位8位
HdmaCh2.Init.Mode = DMA_NORMAL; // 禁止循环模式
HdmaCh2.Init.Priority = DMA_PRIORITY_HIGH; // 通道优先级为高
HAL_DMA_Init(&HdmaCh2); // 初始化DMA通道1
__HAL_LINKDMA(hi2c, hdmarx, HdmaCh2); // DMA1 关联 IIC_RX
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 1); // 中断优先级设置
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); // 使能DMA通道1中断
HAL_NVIC_SetPriority(DMA1_Channel2_3_IRQn, 0, 1); // 中断优先级设置
HAL_NVIC_EnableIRQ(DMA1_Channel2_3_IRQn); // 使能DMA通道2_3中断
}
#endif
主从机的这部分代码相同,请参考我的上一篇《普冉(PUYA)单片机开发笔记(10): I2C通信-配置从机-CSDN博客》中的说明,这里不再重复。
4. 重定向 I2C_IRQHandler
#include "main.h"
#include "py32f0xx_it.h"
...
...
#if(I2C_COMM_TEST)
extern I2C_HandleTypeDef I2cHandle; // Defined in app_i2c.c
#endif
...
...
void DMA1_Channel1_IRQHandler(void)
{
#if(ADC_SAMPLE_TEST)
HAL_DMA_IRQHandler(hadcdma.DMA_Handle);
#endif
#if(I2C_COMM_TEST)
HAL_DMA_IRQHandler(I2cHandle.hdmatx);
#endif
}
void DMA1_Channel2_3_IRQHandler(void)
{
#if(I2C_COMM_TEST)
HAL_DMA_IRQHandler(I2cHandle.hdmarx);
#endif
}
...
...
void I2C1_IRQHandler(void)
{
#if(I2C_SLAVE)
HAL_I2C_ER_IRQHandler(&I2cHandle);
#endif
HAL_I2C_EV_IRQHandler(&I2cHandle);
}
...
...
这部分的修改,主从机也差不多相同。
5. main.c 中进行调用
int main(void)
{
HAL_Init(); // systick初始化
SystemClock_Config(); // 配置系统时钟
if(USART_Config() != HAL_OK) Error_Handler();
printf("\r\n\r\n\r\n"
"[SYS_INIT] Debug port initilaized.\r\n");
#if(I2C_COMM_TEST)
if(app_i2c_init() != HAL_OK) Error_Handler();
printf("[SYS_INIT] I2C initialized.\r\n");
#endif
printf("\r\n+---------------------------------------+"
"\r\n| PY32F003 MCU is ready. |"
"\r\n+---------------------------------------+"
"\r\n 10 digits sent to you! "
"\r\n+---------------------------------------+"
"\r\n");
HAL_Delay(0);
if (DBG_UART_Start() != HAL_OK) Error_Handler();
HAL_Delay(0);
#if(I2C_COMM_TEST)
#if(I2C_SLAVE)
printf("[SYS_INIT] Slave listening...\r\n");
app_i2c_slave_receive(); app_i2c_wait();
app_i2c_slave_transmit(); app_i2c_wait();
#else
printf("[SYS_INIT] Press User button...\r\n");
while (BSP_PB_GetState(BUTTON_KEY) == 1) { };
app_i2c_master_transmit(); // app_i2c_wait();
app_i2c_master_receive(); // app_i2c_wait();
#endif
#endif
while (1)
{
BSP_LED_Toggle(LED3);
HAL_Delay(500);
}
}
请注意 app_i2c_master_transmit(); 和 app_i2c_master_receive(); 这两句后面的 app_i2c_wait(); 被注释掉了,这是因为这两个函数被移到了 xxx_transmit(); 和 xxx_receive(); 中去了。
实验结果
- 定义 I2C_SLAVE 为 1,编译和烧录从机一侧的代码,在从机开发板上观察 LED 亮起又熄灭,说明代码运行到了 I2C 接收处,同时从 XCOM 的 COM3 窗口可以看到 “Slave listening...”的打印。
- 定义 I2C_SLAVE 为 0,编译和烧录主机一侧的代码,在主机开发板上观察 LED 亮起又熄灭,从 XCOM 的 COM32 窗口可以看到 “Press User button...” 的提示。
- 按下主机的“用户按钮”(注意不是复位按钮),观察到主机和从机窗口有预计的打印。
- 复位从机,再复位主机,按下主机的用户按钮,收发的打印可以复现。
实验的串口截图如下所示。
总结
- 使用 PY32F003 进行 I2C 的主从机交互的配置比较繁杂,需要为 I2C 分配复用端口,分配中断优先级,分配 DMA 资源才能顺利完成。占用资源不少,如果使用了 I2C,DMA 就不能用于其它功能了。
- 配置 DMA 时,要注意 TX/RX 两个方向的 DMA 搬运方向要对应,发送通道的搬运方向是内存->外设,接收通道的搬运方向是外设->内存。
- PY32F003 的 I2C 使用 DMA 的单字节缓冲,因此 DMA 的PeriphDataAlignment 和 MemDataAlignment 都要设置为 BYTE 型。实测如果设置为 HALF_WORD 或者 WORD 会卡死。
- PY32F003 的 I2C 管脚是 PA3/PA2, PA7/PA12,PB6/PB7,它们属于不同 AF 组,如果有其它的功能需要集成在一起,需要仔细地分配这些管脚,避免复用的冲突。
- 主从机通信时,它们必须可靠地共地。实测共地不牢靠,会在收发时卡死。不过在实用产品设计中做到这一点倒不难 ;)
- 作为一颗直连硬件的 MCU,PY32F003 在 I2C 通信中往往充当从机的角色。
因为我的实验都用这个工程,以前的实验用到了 TIM, PWM,DMA,Flash 等,本文整理的代码,尤其是初始化代码,颇费了一些周折。后来还是使用预编译开关把用不到的代码都 #if(0) 掉了才得以顺利运行。
也是的,在实用中,每一个 MCU 的功能一般是较为单一的,无非就是和上位机交换报文,根据通信协议解析报文,然后要么模拟采样,要么PWM输出,要么控制GPIO。上位机通信接口往往也是单一的,要么是 UART, 要么是 I2C,连 SPI 用的都很不多。在 PY32F003 这样一颗 Cortex-M0+ 内核的芯片上完成多种复杂的功能,不免有些为难人家了。实用中多种功能集成在一起的场合也不常见。