You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Proteus仿真ATmega328P调用USART_RX_vect中断出现无效操作码错误

ATmega328P(Arduino NANO)蓝牙控制伺服电机/水泵代码仿真故障排查

问题说明

我为ATmega328P(Arduino NANO)编写了通过HC-10蓝牙模块控制伺服电机和水泵的代码,编译器编译无错误,但仿真无法运行。之前调用不带inline关键字的函数时也出现过同样问题,现在调用USART_RX_vect中断再次出现该问题。我是嵌入式开发新手,希望得到帮助。

原代码

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

enum {
  SERVO_MAX_RIGHT   = 500,
  SERVO_MAX_LEFT    = 248,
  SERVO_MID         = 375,
  BAUD              = 9600,
  FOSC              = 16000000,
  MYUBRR            = FOSC/16/BAUD-1,
  BUT_L             = 97,           // 左转按键ASCII码
  BUT_R             = 100,          // 右转按键ASCII码
  BUT_M             = 115,          // 水泵启停按键ASCII码
};

struct button {
unsigned char leftOn;
unsigned char rightOn;
unsigned char moveOn;
unsigned char pressed;
} button;

static inline void portInit() {
  DDRC &= ~(1<<DDC0);               // 设置按键引脚为输入
  DDRC &= ~(1<<DDC1);                 
  DDRC &= ~(1<<DDC2);                
  PORTC |= (1<<PORTC0);             // 按键引脚启用上拉电阻
  PORTC |= (1<<PORTC1);
  PORTC |= (1<<PORTC2);

  DDRB |= (1<<PB0);                 // 设置水泵引脚为输出
  PORTB &= ~(1<<PB0);               // 水泵初始状态关闭
  DDRB |= (1<<PB1);                 // 设置伺服电机引脚为输出
  PORTB &= ~(1<<PB1);               // 伺服引脚初始电平低
}

static inline void timerInit() {
  unsigned char sreg;

  TCCR1A |= (1<<COM1A1);            // 配置非反相PWM模式
  TCCR1A &= ~(1<<COM1A0); 
  TCCR1A &= ~(1<<WGM10);            // 配置Fast PWM模式(ICR1为TOP)
  TCCR1A |= (1<<WGM11);
  TCCR1B |= (1<<WGM12);
  TCCR1B |= (1<<WGM13);
  TCCR1B |= (1<<CS10);              // 设置预分频器为64
  TCCR1B |= (1<<CS11);
  TCCR1B &= ~(1<<CS12);

  sreg = SREG;                      // 保存全局中断状态
  cli();
  ICR1 = 5000;                      // 设置PWM周期(20ms,对应50Hz)

  SREG = sreg;                      // 恢复全局中断状态
}

static inline void uartInit(unsigned int ubrr) {
  UBRR0H = (unsigned char) (ubrr>>8); // 设置波特率高字节
  UBRR0L = (unsigned char)ubrr;     // 设置波特率低字节
  UCSR0B = 0b10011000;              // 启用RX、TX及RX中断
  UCSR0C = ~(1<<USBS0);             // 设置1个停止位
  UCSR0C = ~(1<<UCSZ02);            // 配置8位数据位(此处错误,直接赋值会覆盖之前设置)
  UCSR0C = (1<<UCSZ01);             
  UCSR0C = (1<<UCSZ00);
}

static inline void uart_send(char* str) {
  while(*str) {
    while(UCSR0A & (1<<UDRE0)) {    // 逻辑错误:应该等待UDRE0置位,当前条件写反
      UDR0 = *str;
      str++;
    }
  }
}

static inline void uart_boatControl(unsigned char read) {
  uint8_t val;
  if(read) {
    val = UDR0;
    switch(val) {
      case BUT_L:
        OCR1A = SERVO_MAX_LEFT; 
        break;
      case BUT_R:
        OCR1A = SERVO_MAX_RIGHT;
        break;
      case BUT_M:
        PORTB = (1<<PB0);           // 错误:直接赋值会覆盖PB1引脚状态
        break;
      default:
        OCR1A = SERVO_MID;
        PORTB = ~(1<<PB0);          // 错误:直接赋值会翻转所有引脚,包括PB1
    }

  }
}

ISR(USART_RX_vect){
  button.pressed = UCSR0A & (1<<RXC0); // 冗余:ISR触发时RXC0必然置位
  uart_boatControl(button.pressed); 
}

int main(void)  
{
  portInit();
  timerInit();

  OCR1A = SERVO_MID;                // 伺服初始位置居中
  _delay_ms(500);

  uartInit(MYUBRR);

  while(1) {
    button.leftOn = (!(PINC&(1<<PC0)));
    button.rightOn = (!(PINC&(1<<PC1)));
    button.moveOn = (!(PINC&(1<<PC2)));

    if (button.leftOn) {
      OCR1A = SERVO_MAX_LEFT; 
    } else if (button.rightOn) {
      OCR1A = SERVO_MAX_RIGHT;
    } else if (button.moveOn) {
      PORTB |= (1<<PB0);
    } else {
      OCR1A = SERVO_MID;
      PORTB &= ~(1<<PB0);
    }
  }

  return 0;
}

关键问题及修复方案

1. 全局中断未启用

USART接收中断需要全局中断开启才能触发,主函数中初始化UART后需添加sei();开启全局中断。

2. UCSR0C寄存器配置错误

多次直接赋值UCSR0C会覆盖之前的配置,正确做法是用位操作组合配置:

UCSR0C &= ~(1<<USBS0);             // 设置1个停止位
UCSR0C |= (1<<UCSZ01) | (1<<UCSZ00); // 配置8位数据位

3. uart_send函数逻辑错误

等待发送缓冲区为空的条件写反了,正确写法:

while(!(UCSR0A & (1<<UDRE0)));     // 等待发送缓冲区为空
UDR0 = *str;
str++;

4. 中断函数冗余判断

USART_RX_vect触发时,必然有数据接收,无需判断RXC0位,直接读取UDR0即可,同时避免主循环和中断的资源竞争:

ISR(USART_RX_vect){
  uint8_t val = UDR0;
  switch(val) {
    case BUT_L:
      OCR1A = SERVO_MAX_LEFT; 
      break;
    case BUT_R:
      OCR1A = SERVO_MAX_RIGHT;
      break;
    case BUT_M:
      PORTB ^= (1<<PB0);           // 改用翻转操作,不影响其他引脚
      break;
    default:
      OCR1A = SERVO_MID;
      PORTB &= ~(1<<PB0);          // 仅关闭水泵,不影响伺服引脚
  }
}

5. PORTB直接赋值的风险

直接赋值PORTB会修改所有引脚状态,应该用位操作|=或&=来单独控制PB0(水泵),避免影响PB1(伺服)的PWM输出。

6. inline函数的误解

inline只是编译器的优化建议,不会导致仿真失败,之前的问题本质是代码逻辑错误,和是否加inline无关。

修正后的完整代码

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

enum {
  SERVO_MAX_RIGHT   = 500,
  SERVO_MAX_LEFT    = 248,
  SERVO_MID         = 375,
  BAUD              = 9600,
  FOSC              = 16000000,
  MYUBRR            = FOSC/16/BAUD-1,
  BUT_L             = 97,           // 左转按键ASCII码
  BUT_R             = 100,          // 右转按键ASCII码
  BUT_M             = 115,          // 水泵启停按键ASCII码
};

struct button {
unsigned char leftOn;
unsigned char rightOn;
unsigned char moveOn;
} button;

static inline void portInit() {
  DDRC &= ~(1<<DDC0) & ~(1<<DDC1) & ~(1<<DDC2); // 批量设置按键引脚为输入
  PORTC |= (1<<PORTC0) | (1<<PORTC1) | (1<<PORTC2); // 批量启用上拉电阻

  DDRB |= (1<<PB0) | (1<<PB1);     // 批量设置水泵、伺服引脚为输出
  PORTB &= ~(1<<PB0);               // 水泵初始关闭
}

static inline void timerInit() {
  unsigned char sreg = SREG;
  cli();

  TCCR1A = (1<<COM1A1) | (1<<WGM11); // 配置非反相PWM + Fast PWM模式
  TCCR1B = (1<<WGM12) | (1<<WGM13) | (1<<CS10) | (1<<CS11); // Fast PWM + 预分频64
  ICR1 = 5000;                      // 设置20ms周期

  SREG = sreg;
}

static inline void uartInit(unsigned int ubrr) {
  UBRR0H = (unsigned char)(ubrr >> 8);
  UBRR0L = (unsigned char)ubrr;
  UCSR0B = (1<<RXEN0) | (1<<TXEN0) | (1<<RXCIE0); // 启用RX、TX、RX中断
  UCSR0C = (1<<UCSZ01) | (1<<UCSZ00); // 8位数据位,1个停止位(默认)
}

static inline void uart_send(char* str) {
  while(*str) {
    while(!(UCSR0A & (1<<UDRE0))); // 等待发送缓冲区就绪
    UDR0 = *str++;
  }
}

ISR(USART_RX_vect){
  uint8_t val = UDR0;
  switch(val) {
    case BUT_L:
      OCR1A = SERVO_MAX_LEFT; 
      break;
    case BUT_R:
      OCR1A = SERVO_MAX_RIGHT;
      break;
    case BUT_M:
      PORTB ^= (1<<PB0);           // 翻转水泵状态
      break;
    default:
      OCR1A = SERVO_MID;
      PORTB &= ~(1<<PB0);          // 关闭水泵
  }
}

int main(void)  
{
  portInit();
  timerInit();

  OCR1A = SERVO_MID;
  _delay_ms(500);

  uartInit(MYUBRR);
  sei(); // 开启全局中断

  while(1) {
    button.leftOn = !(PINC & (1<<PC0));
    button.rightOn = !(PINC & (1<<PC1));
    button.moveOn = !(PINC & (1<<PC2));

    if (button.leftOn) {
      OCR1A = SERVO_MAX_LEFT; 
    } else if (button.rightOn) {
      OCR1A = SERVO_MAX_RIGHT;
    } else if (button.moveOn) {
      PORTB |= (1<<PB0);
    } else {
      // 仅在无按键操作时才恢复默认状态,避免和中断冲突
      if((OCR1A != SERVO_MID) || (PORTB & (1<<PB0))) {
        OCR1A = SERVO_MID;
        PORTB &= ~(1<<PB0);
      }
    }
  }

  return 0;
}

内容的提问来源于stack exchange,提问作者vpos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 05:05:00