PIC单片机C语言编程实例如何快速上手?

99ANYc3cd6
预计阅读时长 32 分钟
位置: 首页 C语言 正文

准备工作

在开始编程之前,你需要以下工具:

pic单片机c语言编程实例
(图片来源网络,侵删)
  1. 硬件:

    • 一块PIC16F877A开发板(或最小系统板)。
    • USB转TTL/串口模块(用于程序下载,如PICkit 3/4或简单的CH340G模块)。
    • LED灯(通常开发板已自带)。
    • 按键。
    • 10kΩ 电阻(用于按键上拉)。
    • 面包板和杜邦线。
  2. 软件:

    • MPLAB X IDE: Microchip的官方集成开发环境。
    • XC8 Compiler: Microchip的C语言编译器(有免费版)。

点亮一个LED (GPIO输出)

这是所有单片机编程的 "Hello, World!",我们将让连接在RB0引脚上的LED闪烁。

硬件连接

  • PIC16F877A的 RB0 (PORTB, bit 0) 引脚连接到LED的正极。
  • LED的负极通过一个限流电阻(如330Ω)连接到GND(地)。

C语言代码 (blink.c)

#include <xc.h>
// 配置位设置 (非常重要!)
// 对于PIC16F877A,使用#FOSC_HS表示使用外部高速晶振
// 禁看门狗等
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_ON & BOREN_OFF & LVP_OFF & CPD_OFF & WRT_OFF & CP_OFF);
#define _XTAL_FREQ 4000000 // 定义晶振频率,这里使用4MHz
void main(void) {
    // 1. 设置TRISB寄存器
    // TRISB是一个方向寄存器,1代表输入,0代表输出
    // 我们想控制RB0,所以将其设为0
    TRISBbits.TRISB0 = 0; // 将RB0设置为输出模式
    // 2. 主循环
    while(1) {
        // 3. 操作PORTB寄存器来控制LED
        // 设置RB0为高电平,点亮LED
        PORTBbits.RB0 = 1;
        // 延时一段时间
        __delay_ms(500); // 延时500毫秒 (需要定义_XTAL_FREQ)
        // 设置RB0为低电平,熄灭LED
        PORTBbits.RB0 = 0;
        // 再次延时
        __delay_ms(500);
    }
}

代码解释

  • #include <xc.h>: 包含所有XC8编译器所需的头文件,定义了寄存器(如TRISB, PORTB)和特殊功能函数。
  • __CONFIG(...): 这是配置位设置,它告诉单片机的硬件如何工作,比如选择哪种时钟源、是否启用看门狗等。对于每个项目,这都是必须的
  • #define _XTAL_FREQ 4000000: 告诉编译器你的系统时钟频率是4MHz,这对于__delay_ms()这类延时函数的精确计算至关重要。
  • TRISBbits.TRISB0 = 0;: TRIS寄存器用于设置I/O引脚的方向。TRISB0TRISB寄存器的第0位,设为0,表示RB0引脚为输出
  • PORTBbits.RB0 = 1;: PORT寄存器用于在输出模式下控制引脚的电平,设为1,RB0输出高电平,LED点亮。
  • __delay_ms(500);: 这是一个XC8提供的内置函数,用于产生精确的毫秒级延时。必须定义_XTAL_FREQ才能使用
  • while(1): 这是一个无限循环,确保程序持续运行,LED不断闪烁。

读取按键状态 (GPIO输入)

我们将读取连接在RB1引脚上的按键状态,并在RB0的LED上显示出来(按键按下时LED亮)。

pic单片机c语言编程实例
(图片来源网络,侵删)

硬件连接

  • RB0 -> LED -> 电阻 -> GND (同实例一)
  • RB1 -> 按键一端
  • 按键另一端 -> GND
  • RB1VDD (5V) 之间接一个10kΩ上拉电阻,这样按键未按下时,RB1为高电平;按下时,RB1被拉到低电平。

C语言代码 (button_led.c)

#include <xc.h>
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_ON & BOREN_OFF & LVP_OFF & CPD_OFF & WRT_OFF & CP_OFF);
#define _XTAL_FREQ 4000000
void main(void) {
    // 1. 配置引脚方向
    TRISBbits.TRISB0 = 0; // RB0为输出
    TRISBbits.TRISB1 = 1; // RB1为输入
    // 2. 配置RB1内部上拉电阻 (可选,但推荐)
    // 当引脚设置为输入时,可以启用内部上拉电阻,这样就可以省略外部上拉电阻
    OPTION_REGbits.nRBPU = 0; // 禁止PORTB上拉锁存器,从而启用内部上拉
    WPUBbits.WPUB1 = 1;       // 为RB1启用内部上拉
    while(1) {
        // 3. 读取按键状态
        // 如果RB1为低电平 (按键按下)
        if (PORTBbits.RB1 == 0) {
            PORTBbits.RB0 = 1; // 点亮LED
        } else {
            // 如果RB1为高电平 (按键未按下)
            PORTBbits.RB0 = 0; // 熄灭LED
        }
    }
}

代码解释

  • TRISBbits.TRISB1 = 1;: 将RB1设置为输入模式。
  • OPTION_REGbits.nRBPU = 0;: OPTION_REG是一个特殊功能寄存器。nRBPU位控制PORTB的所有上拉电阻,将其设为0,表示使能PORTB的上拉功能。
  • WPUBbits.WPUB1 = 1;: WPUB寄存器用于单独控制PORTB每个引脚的上拉电阻。WPUB1 = 1表示为RB1引脚启用内部上拉电阻。
  • if (PORTBbits.RB1 == 0): 读取PORTB寄存器的RB1位,因为按键按下时引脚被拉低,所以检查是否为0。
    • 注意: 如果没有使用内部上拉,而是外部上拉,读取逻辑不变。

使用定时器中断实现精确闪烁

上面的__delay_ms()虽然方便,但在延时期间,CPU在空转,无法执行其他任务,使用定时器中断可以实现非阻塞的、更精确的定时。

C语言代码 (timer_interrupt.c)

#include <xc.h>
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_ON & BOREN_OFF & LVP_OFF & CPD_OFF & WRT_OFF & CP_OFF);
#define _XTAL_FREQ 4000000
// 全局变量,用于在中断服务程序和主程序之间传递数据
volatile unsigned int led_counter = 0;
// 中断服务程序
void __interrupt() myISR(void) {
    // 检查是否是TMR0溢出中断
    if (INTCONbits.T0IF) {
        // 1. 清除中断标志位,非常重要!
        INTCONbits.T0IF = 0;
        // 2. 重新加载TMR0的初值,以产生固定的中断间隔
        // 假设我们希望每0.025秒中断一次
        // 4MHz晶振,TMR0预分频比为1:256
        // TMR0是一个8位定时器,最大计数值为256
        // 中断频率 = 4MHz / 256 / (256 - 初值)
        // 我们想得到40Hz (0.025s) 的中断
        // 4000000 / 256 / (256 - X) = 40
        // 256 - X = 4000000 / 256 / 40 = 387.5
        // X = 256 - 387.5 (负数,说明需要调整思路)
        // 更简单的计算:每次计数增加 1,需要 256 - X 次计数才溢出。
        // 我们希望 256 - X = (4MHz / 256) / 40 = 387.5,这不可能,因为X必须小于256。
        // 我们调整目标,比如每1ms中断一次。
        // 4000000 / 256 / (256 - X) = 1000
        // 256 - X = 4000000 / 256 / 1000 = 15.6
        // X = 256 - 16 = 240
        TMR0 = 240; // 重新加载初值
        // 3. 执行中断任务
        led_counter++;
        // 每40次中断 (40 * 1ms = 40ms) 切换一次LED,实现1Hz闪烁
        if (led_counter >= 40) {
            PORTBbits.RB0 = ~PORTBbits.RB0; // 取反LED状态
            led_counter = 0;
        }
    }
}
void main(void) {
    // 1. 配置TMR0
    T0CS = 0;      // TMR0由内部指令时钟驱动
    PSA = 0;       // 分频器分配给TMR0
    PS2 = PS1 = PS0 = 1; // 设置分频比为1:256
    // 2. 配置中断
    GIE = 1;       // 全局中断使能
    T0IE = 1;      // TMR0溢出中断使能
    // 3. 配置LED引脚
    TRISBbits.TRISB0 = 0; // RB0为输出
    PORTBbits.RB0 = 0;   // 初始状态为熄灭
    // 4. 加载TMR0初值
    TMR0 = 240;
    // 5. 主循环
    while(1) {
        // 主循环可以执行其他任务,比如处理按键、通信等
        // 这里我们让它空转,因为所有定时任务都在中断里完成了
    }
}

代码解释

  • volatile unsigned int led_counter = 0;: volatile关键字非常重要!它告诉编译器这个变量可能会在程序的其他地方(比如中断服务程序中被改变),防止编译器优化掉看似“不必要”的访问。
  • void __interrupt() myISR(void): 这是中断服务程序的固定写法。
  • if (INTCONbits.T0IF): 检查TMR0的中断标志位是否被置位,如果被置位,说明TMR0已经溢出。
  • INTCONbits.T0IF = 0;: 必须手动清除中断标志位,否则CPU会立即再次进入中断,导致程序卡死。
  • TMR0 = 240;: 在中断服务程序中重新给TMR0赋值,这样它就会从240开始重新计数,再次溢出时就会再次触发中断,形成一个周期性的定时器。
  • GIE = 1;: 全局中断使能位,是开启所有中断的总开关。
  • T0IE = 1;: TMR0溢出中断使能位,是开启TMR0中断的开关。
  • while(1): 主循环现在是空闲的,CPU可以响应其他事件,实现了多任务处理。

进阶实例:驱动LCD1602字符液晶

LCD1602是常用的显示模块,可以显示两行,每行16个字符,这需要更复杂的时序控制,通常使用4位或8位模式,这里我们使用4位模式以节省I/O口。

硬件连接 (4位模式)

LCD Pin Function PIC16F877A Pin
VSS GND GND
VDD +5V VDD
V0 对比度 (接电位器中间脚) -
RS 寄存器选择 RB4
RW 读/写 (通常接地) GND
EN 使能 RB5
D4 数据线 4 RB6
D5 数据线 5 RB7
D6 数据线 6 RB8
D7 数据线 7 RB9
A 背光正极 +5V
K 背光负极 GND

C语言代码 (lcd1602.c)

#include <xc.h>
#include <string.h> // 用于strlen函数
__CONFIG(FOSC_HS & WDTE_OFF & PWRTE_ON & BOREN_OFF & LVP_OFF & CPD_OFF & WRT_OFF & CP_OFF);
#define _XTAL_FREQ 4000000
// LCD引脚定义
#define LCD_RS RB4
#define LCD_EN RB5
#define LCD_D4 RB6
#define LCD_D5 RB7
#define LCD_D6 RB8
#define LCD_D7 RB9
// 函数声明
void LCD_Init();
void LCD_Cmd(unsigned char cmd);
void LCD_WriteChar(unsigned char dat);
void LCD_String(const char *str);
void LCD_SetCursor(unsigned char row, unsigned char col);
void main(void) {
    // 设置所有LCD引脚为输出
    TRISB4 = 0; TRISB5 = 0; TRISB6 = 0; TRISB7 = 0; TRISB8 = 0; TRISB9 = 0;
    LCD_Init(); // 初始化LCD
    LCD_String("Hello, PIC!"); // 显示第一行字符串
    LCD_SetCursor(2, 1);      // 移动到第二行,第1列
    LCD_String("This is LCD"); // 显示第二行字符串
    while(1) {
        // 主循环保持程序运行
    }
}
// 发送4位数据到LCD
void LCD_Send4Bits(unsigned char data) {
    LCD_D4 = (data & 0x01) ? 1 : 0;
    LCD_D5 = (data & 0x02) ? 1 : 0;
    LCD_D6 = (data & 0x04) ? 1 : 0;
    LCD_D7 = (data & 0x08) ? 1 : 0;
}
// 发送命令到LCD
void LCD_Cmd(unsigned char cmd) {
    LCD_RS = 0; // RS=0, 表示发送命令
    LCD_Send4Bits(cmd >> 4); // 发送高4位
    LCD_PulseEnable();
    LCD_Send4Bits(cmd);     // 发送低4位
    LCD_PulseEnable();
    __delay_ms(2);
}
// 发送字符到LCD
void LCD_WriteChar(unsigned char dat) {
    LCD_RS = 1; // RS=1, 表示发送数据
    LCD_Send4Bits(dat >> 4); // 发送高4位
    LCD_PulseEnable();
    LCD_Send4Bits(dat);     // 发送低4位
    LCD_PulseEnable();
    __delay_ms(2);
}
// 使能脉冲
void LCD_PulseEnable() {
    LCD_EN = 1;
    __delay_us(1);
    LCD_EN = 0;
    __delay_us(100);
}
// 初始化LCD
void LCD_Init() {
    __delay_ms(50); // 等待LCD上电稳定
    LCD_Cmd(0x33);
    LCD_Cmd(0x32);
    LCD_Cmd(0x28); // 4-bit mode, 2 lines, 5x8 dots
    LCD_Cmd(0x0C); // Display on, cursor off, blink off
    LCD_Cmd(0x06); // Entry mode set - increment no shift
    LCD_Cmd(0x01); // Clear display
    __delay_ms(2);
}
// 在指定位置显示字符串
void LCD_String(const char *str) {
    for (int i = 0; str[i] != '\0'; i++) {
        LCD_WriteChar(str[i]);
    }
}
// 设置光标位置
void LCD_SetCursor(unsigned char row, unsigned char col) {
    unsigned char row_offsets[] = {0x00, 0x40}; // 第一行偏移0,第二行偏移0x40
    LCD_Cmd(0x80 | (col + row_offsets[row]));
}

代码解释

  • 引脚宏定义: 使用宏定义让代码更清晰、易于修改。
  • LCD_Send4Bits(): 将一个字节数据的高4位或低4位分别送到D4-D7这4条数据线上。
  • LCD_PulseEnable(): LCD的使能信号需要一个高脉冲来锁存数据,这个函数产生这个脉冲。
  • LCD_Cmd(): 发送命令,首先将RS置低,然后分两次(先高4位,后低4位)发送8位命令,并在每次发送后产生一个使能脉冲。
  • LCD_WriteChar(): 发送字符,与LCD_Cmd()类似,只是将RS置高。
  • LCD_Init(): LCD上电后需要一系列初始化命令才能正常工作,这些命令时序在LCD的数据手册中有详细说明。
  • LCD_String(): 遍历字符串,逐个字符调用LCD_WriteChar()进行显示。
  • LCD_SetCursor(): 通过发送特定的命令(0x80 | address)来设置光标的位置。0x80是设置DDRAM地址的命令,address是具体的地址(第一行从0x00开始,第二行从0x40开始)。

这些实例涵盖了PIC单片机C语言编程的核心概念:

  1. GPIO控制: 输入/输出方向设置,读写操作。
  2. 延时函数: __delay_ms()的简单使用。
  3. 中断: 定时器中断的配置和使用,实现非阻塞式编程。
  4. 外设驱动: 驱动像LCD这样的复杂外设,需要精确的时序控制。

从这些基础开始,你可以逐步学习更复杂的功能,如ADC模数转换、PWM脉宽调制、SPI/I2C通信等,实践是最好的老师,动手搭建电路、编写代码、调试问题,你的技能会飞速提升。

pic单片机c语言编程实例
(图片来源网络,侵删)
-- 展开阅读全文 --
头像
dede缩略图地址怎么修改?
« 上一篇 03-05
织梦首页如何调用单页内容?
下一篇 » 03-05
取消
微信二维码
支付宝二维码

目录[+]