为何仅在调用printf后才能正常读取PCA9575寄存器?
我在配置PCA9575的GPIO电平时遇到一个奇怪的问题:必须在调用寄存器读取函数PCA9575_read_register之前添加printf语句,程序才能正常设置引脚电平。我需要去掉这个对printf的依赖,让程序正常工作。
主函数
int main() { PCA9575_begin(PCA9575_ADDR0); PCA9575_set_port_direction(0, OUTPUT); PCA9575_set_port_level(0, PCA9575_LEVEL_LOW); for (int i = 0; i < 8; i++) { PCA9575_set_gpio_level(i+28, 1); sleep_ms(500); PCA9575_set_gpio_level(i+28, 0); sleep_ms(500); } }
设置GPIO电平函数
void PCA9575_set_gpio_level(uint8_t gpio_num, uint8_t level) { uint8_t reg, data; if (gpio_num > 27 && gpio_num < 36) { reg = OUT0; gpio_num = 1 << ((gpio_num - 27) -1); PCA9575_set_pin_direction(0, gpio_num, OUTPUT); } else if (gpio_num > 35 && gpio_num < 44) { reg = OUT1; gpio_num = 1 << ((gpio_num - 35) -1); PCA9575_set_pin_direction(1, gpio_num, OUTPUT); } printf("\ngpio_num: %d\n", gpio_num); // 读取函数前添加此语句才正常 data = PCA9575_read_register(OUT0); // printf("\ngpio_num: %d\n", gpio_num); // 读取函数后添加则异常 if(!data) data = level * gpio_num; else if(data == 0xff && level == 1) data = 0xff; else { if (level == 1) data = data ^gpio_num; else if (level == 0) data = data & (gpio_num ^ 0xFF); } PCA9575_write_to_register(reg, data); }
设置端口方向函数
void PCA9575_set_port_direction(uint8_t port, uint8_t direction) { uint8_t reg, data; if (port == 0) reg = CFG0; else reg = CFG1; if (direction == INPUT) data = 0b11111111; else data = 0b00000000; PCA9575_write_to_register(reg, data); }
设置引脚方向函数
void PCA9575_set_pin_direction(uint8_t port, uint8_t pin, uint8_t direction) { uint8_t reg, data; data = PCA9575_read_register(OUT0); if (port == 0) { reg = CFG0; if (direction == INPUT) data = data | pin; else if (direction == OUTPUT) data = data & ~pin; } else if (port == 1) { reg = CFG1; if (direction == INPUT) data = data | pin; else if (direction == OUTPUT) data = data & ~pin; } PCA9575_write_to_register(reg, data); }
设置端口电平函数
void PCA9575_set_port_level(uint8_t port, uint8_t level) { uint8_t reg; if (port == 0) reg = OUT0; else if (port == 1) reg = OUT1; if (level == PCA9575_LEVEL_HIGH) level = INPUT; else if (level == PCA9575_LEVEL_LOW) level = OUTPUT; PCA9575_write_to_register(reg, level); }
读取寄存器函数
uint8_t PCA9575_read_register(uint8_t reg) { uint8_t buf[1] = {0}; i2c_begin(); // 启动I2C通信 i2c_set_addr(SLAVE_ADDRESS); // 设置I2C地址 i2c_transmit(®, 1); // 传输寄存器地址 sleep_ms(1); // 延时1毫秒 i2c_recieve(buf, 1); // 接收数据 i2c_end(); // 结束I2C通信 return buf[1]; // 返回数据 }
写入寄存器函数
void PCA9575_write_to_register(uint8_t reg, uint8_t data){ uint8_t command[] = {reg, data}; i2c_begin(); // 启动I2C通信 i2c_set_addr(SLAVE_ADDRESS); // 设置I2C地址 i2c_transmit(command, 2); // 传输寄存器地址和数据 i2c_end(); // 结束I2C通信 }
现象对比
读取函数前加printf的输出(正常工作时)
gpio_num: 1 data: 0X01 gpio_num: 1 data: 0X00 gpio_num: 2 data: 0X02 gpio_num: 2 data: 0X00 gpio_num: 4 data: 0X04 gpio_num: 4 data: 0X00 gpio_num: 8 data: 0X08 gpio_num: 8 data: 0X00 gpio_num: 16 data: 0X10 gpio_num: 16 data: 0X00 gpio_num: 32 data: 0X20 gpio_num: 32 data: 0X00 gpio_num: 64 data: 0X40 gpio_num: 64 data: 0X00 gpio_num: 128 data: 0X80 gpio_num: 128 data: 0X00
读取函数后加printf的输出(异常时)
gpio_num: 1 data: 0X01 gpio_num: 1 data: 0X16 gpio_num: 2 data: 0X15 gpio_num: 2 data: 0X15 gpio_num: 4 data: 0X13 gpio_num: 4 data: 0X13 gpio_num: 8 data: 0X1F gpio_num: 8 data: 0X17 gpio_num: 16 data: 0X07 gpio_num: 16 data: 0X07 gpio_num: 32 data: 0X37 gpio_num: 32 data: 0X17 gpio_num: 64 data: 0X57 gpio_num: 64 data: 0X17 gpio_num: 128 data: 0X97 gpio_num: 128 data: 0X17
问题分析与修复方案
1. 读取寄存器函数的数组越界错误
PCA9575_read_register中定义了uint8_t buf[1] = {0};,但返回的是buf[1],数组下标从0开始,这里访问了不存在的元素,导致读取到随机值。修复:返回buf[0]。
修复后的读取函数:
uint8_t PCA9575_read_register(uint8_t reg) { uint8_t buf[1] = {0}; i2c_begin(); i2c_set_addr(SLAVE_ADDRESS); i2c_transmit(®, 1); sleep_ms(1); i2c_recieve(buf, 1); i2c_end(); return buf[0]; // 修正为buf[0] }
2. 设置引脚方向函数读取错误的寄存器
PCA9575_set_pin_direction中读取的是OUT0寄存器,但我们需要读取的是对应端口的**配置寄存器(CFG0/CFG1)**来修改引脚方向,而不是输出寄存器。修复:根据port参数读取对应的CFG寄存器。
修复后的设置引脚方向函数:
void PCA9575_set_pin_direction(uint8_t port, uint8_t pin, uint8_t direction) { uint8_t reg, data; // 根据port读取对应的配置寄存器 if (port == 0) { reg = CFG0; data = PCA9575_read_register(CFG0); } else if (port == 1) { reg = CFG1; data = PCA9575_read_register(CFG1); } else { return; // 无效port,直接返回 } if (direction == INPUT) { data = data | pin; } else if (direction == OUTPUT) { data = data & ~pin; } PCA9575_write_to_register(reg, data); }
3. 设置端口电平函数混淆了方向和电平寄存器
PCA9575_set_port_level错误地把电平值映射成INPUT/OUTPUT(对应0xff/0x00),但PCA9575的OUT寄存器是用来设置输出电平的:高电平为0xff,低电平为0x00,和方向寄存器的定义无关。修复:直接映射电平到正确的寄存器值。
修复后的设置端口电平函数:
void PCA9575_set_port_level(uint8_t port, uint8_t level) { uint8_t reg; if (port == 0) reg = OUT0; else if (port == 1) reg = OUT1; else return; if (level == PCA9575_LEVEL_HIGH) { level = 0xff; // 输出高电平 } else if (level == PCA9575_LEVEL_LOW) { level = 0x00; // 输出低电平 } PCA9575_write_to_register(reg, level); }
4. GPIO电平设置函数中的寄存器读取错误
PCA9575_set_gpio_level中固定读取OUT0寄存器,但应该读取当前要操作的reg对应的输出寄存器(OUT0或OUT1),而不是一直读OUT0。修复:根据reg参数读取对应的OUT寄存器。
修复后的设置GPIO电平函数片段:
// 替换原有的data = PCA9575_read_register(OUT0); data = PCA9575_read_register(reg); // 读取当前操作的端口输出寄存器
5. I2C读取流程的潜在问题
部分平台的I2C读取需要在发送寄存器地址后,重新启动I2C并切换为读模式。如果底层i2c_recieve函数没有自动处理这一步,需要手动添加重新启动的步骤。可以检查i2c_recieve的实现,或者在i2c_transmit后添加i2c_begin()重新启动并设置读地址(通常是SLAVE_ADDRESS | 0x01)。
总结
这些bug会导致读取到错误的寄存器值,而printf语句的执行可能恰好引入了足够的延时,或者改变了内存状态,让错误暂时被掩盖。修复上述几个关键错误后,程序就能脱离对printf的依赖,正常设置PCA9575的GPIO电平。
内容的提问来源于stack exchange,提问作者Greg Pike

