基于PIC18 16位MCU的裸机嵌入式系统多任务架构咨询
裸机PIC18系统架构实现方案(基于定时器的任务调度)
针对你的需求,我们可以用硬件定时器+时间片轮询任务的架构来实现,完全不需要操作系统,靠全局时间基准和任务列表来满足时序约束。核心思路是用定时器产生固定周期的中断(比如1ms)维护系统时间戳,主循环轮询各个任务的触发条件,满足就执行对应逻辑。
核心架构设计
- 系统时间基准:用PIC18的硬件定时器(比如Timer0/Timer1)配置1ms中断,在中断服务函数中仅递增全局毫秒计数器,不做其他复杂操作,保证中断响应速度。
- 任务管理:定义任务结构体,包含任务的执行周期、上次执行时间戳、任务函数指针。主循环中轮询所有任务,当当前时间与上次执行时间的差值达到任务周期时,执行任务。
- 模块化拆分:将消抖、CAN发送、定时输出等功能拆分为独立任务,各自维护状态,降低耦合。
具体实现步骤与代码示例
1. 全局变量与结构体定义
#include <pic18.h> #include <stdint.h> // 全局系统毫秒计数器(16位,最大覆盖65.5秒,溢出后无符号减法可自动处理) volatile uint16_t sys_tick_ms = 0; // 任务结构体:描述每个任务的调度参数 typedef struct { uint16_t last_run_tick; // 上次执行的时间戳 uint16_t interval_ms; // 任务执行周期(毫秒) void (*execute)(void); // 任务执行函数指针 } ScheduledTask; // 数字输入消抖结构体:维护每个输入的消抖状态 typedef struct { uint8_t raw_pin; // 原始引脚电平 uint8_t stable_state; // 消抖后的稳定状态 uint8_t debounce_counter; // 消抖计数 const uint8_t threshold; // 消抖阈值(比如5ms=5次连续采样) } DebouncedInput; // 定时输出结构体:维护每个输出的定时状态 typedef struct { uint8_t* output_pin; // 输出引脚指针 uint16_t expire_tick; // 到期时间戳 uint8_t is_active; // 是否处于定时激活状态 } TimedOutput;
2. 定时器中断配置
以Timer0为例(可根据PIC18型号调整定时器),配置1ms中断周期,仅更新时间戳:
void __interrupt() isr(void) { // 处理Timer0中断 if (INTCONbits.TMR0IF) { // 重置定时器计数(需根据你的系统时钟和预分频计算准确值) TMR0 = 0x80; // 示例值,实际需根据Fosc计算 sys_tick_ms++; INTCONbits.TMR0IF = 0; // 清除中断标志位 } // 处理CAN接收中断(仅标记新消息,解析放在主循环) if (PIR3bits.RXB0IF) { can_new_msg_flag = 1; // 全局标志位,标记有新CAN消息 PIR3bits.RXB0IF = 0; } } // 初始化定时器与中断 void init_timer_and_interrupts(void) { // 配置Timer0:预分频、时钟源等 OPTION_REGbits.T0CS = 0; // 内部时钟Fosc/4 OPTION_REGbits.PSA = 0; // 预分频分配给Timer0 OPTION_REGbits.PS = 0b111; // 预分频值1:256 TMR0 = 0x80; // 初始计数,保证1ms中断 // 开启中断 INTCONbits.TMR0IE = 1; // 允许Timer0中断 INTCONbits.GIE = 1; // 开启全局中断 INTCONbits.PEIE = 1; // 允许外设中断(用于CAN) }
3. 任务定义与初始化
// 任务函数声明 void task_debounce_inputs(void); void task_10ms_can_transmit(void); void task_check_timed_outputs(void); // 任务列表:按执行周期排序,优化轮询效率 ScheduledTask tasks[] = { {0, 1, task_debounce_inputs}, // 1ms一次消抖检测 {0, 1, task_check_timed_outputs}, // 1ms一次检查定时输出 {0, 10, task_10ms_can_transmit} // 10ms一次CAN发送 }; const uint8_t task_count = sizeof(tasks) / sizeof(ScheduledTask); // 初始化2路数字输入消抖实例 DebouncedInput dig_inputs[2] = { {PORTBbits.RB0, 0, 0, 5}, // 输入1:RB0,消抖阈值5ms {PORTBbits.RB1, 0, 0, 5} // 输入2:RB1,消抖阈值5ms }; // 初始化2路定时输出实例 TimedOutput timed_outputs[2] = { {&PORTBbits.RB2, 0, 0}, // 输出1:RB2 {&PORTBbits.RB3, 0, 0} // 输出2:RB3 }; // CAN新消息标志 uint8_t can_new_msg_flag = 0;
4. 主循环与任务调度
void main(void) { // 硬件初始化:GPIO、CAN、定时器 init_gpio(); init_can(); init_timer_and_interrupts(); while(1) { // 轮询调度所有任务 for (uint8_t i = 0; i < task_count; i++) { // 无符号减法自动处理sys_tick_ms溢出问题 if ((uint16_t)(sys_tick_ms - tasks[i].last_run_tick) >= tasks[i].interval_ms) { tasks[i].execute(); tasks[i].last_run_tick = sys_tick_ms; } } // 处理CAN接收消息(主循环解析,避免中断阻塞) if (can_new_msg_flag) { process_can_received_msg(); can_new_msg_flag = 0; } } }
5. 各任务具体实现
数字输入消抖任务
void task_debounce_inputs(void) { for (uint8_t i = 0; i < 2; i++) { uint8_t current_raw = dig_inputs[i].raw_pin; if (current_raw == dig_inputs[i].stable_state) { dig_inputs[i].debounce_counter = 0; // 状态一致,重置计数 } else { dig_inputs[i].debounce_counter++; // 连续采样达到阈值,更新稳定状态 if (dig_inputs[i].debounce_counter >= dig_inputs[i].threshold) { dig_inputs[i].stable_state = current_raw; dig_inputs[i].debounce_counter = 0; // 可选:触发输入状态变化的回调逻辑 } } } }
10ms CAN发送任务
void task_10ms_can_transmit(void) { // 采集模拟输入 uint16_t adc_val1 = read_adc(0); // 自定义AD读取函数 uint16_t adc_val2 = read_adc(1); // 获取消抖后的数字输入状态 uint8_t dig_state1 = dig_inputs[0].stable_state; uint8_t dig_state2 = dig_inputs[1].stable_state; // 打包CAN数据帧 uint8_t can_data[8] = { (uint8_t)(adc_val1 >> 8), (uint8_t)adc_val1, (uint8_t)(adc_val2 >> 8), (uint8_t)adc_val2, dig_state1, dig_state2, 0x00, 0x00 }; // 发送CAN消息(自定义CAN发送函数) can_send(0x123, can_data, 8); }
定时输出检查任务
void task_check_timed_outputs(void) { for (uint8_t i = 0; i < 2; i++) { if (timed_outputs[i].is_active) { // 检查是否到达到期时间 if ((uint16_t)(sys_tick_ms - timed_outputs[i].expire_tick) >= 0) { *timed_outputs[i].output_pin = 0; // 关闭输出 timed_outputs[i].is_active = 0; } } } }
CAN消息处理函数
void process_can_received_msg(void) { uint8_t can_data[8]; uint16_t can_id = can_receive(can_data); // 自定义CAN接收函数 // 示例指令格式:ID=0x456,字节0=输出通道,字节1-2=定时时长x秒,字节3=动作(1=开启/0=关闭) if (can_id == 0x456) { uint8_t channel = can_data[0]; if (channel < 2) { uint16_t x_seconds = (can_data[1] << 8) | can_data[2]; uint8_t action = can_data[3]; if (action == 1) { *timed_outputs[channel].output_pin = 1; // 开启输出 timed_outputs[channel].expire_tick = sys_tick_ms + x_seconds * 1000; timed_outputs[channel].is_active = 1; } else { *timed_outputs[channel].output_pin = 0; // 强制关闭 timed_outputs[channel].is_active = 0; } } } }
关键注意事项
- 中断优先级:如果使用多个中断(比如CAN接收),要配置PIC18的中断优先级,保证定时器中断优先执行,避免时间基准不准。
- AD采集优化:如果AD转换耗时较长,可以在10ms任务中先启动AD转换,用AD中断标记转换完成,主循环中读取结果,避免阻塞任务调度。
- 变量原子性:
sys_tick_ms是中断修改的全局变量,主循环中读取时如果是16位,要保证原子操作(PIC18读取16位变量时会自动原子化,无需额外处理)。 - 任务执行时间:每个任务的执行时间必须远小于任务周期,比如1ms任务的执行时间要控制在几百微秒内,避免影响其他任务的时序。
内容的提问来源于stack exchange,提问作者S_A1980
相关产品推荐
相关产品推荐

