裸机 + 寄存器 & AVR-GCC 学习之 “L293D 电机驱动 H 桥调速与小车运动控制”
学习笔记:本文基于 HL-2 开发板 + PC 口电机引脚、HL-1 小车 L293D 驱动底盘,采用纯寄存器零库开发,讲解 L293D H 桥工作原理、IN 方向引脚、EN 使能 PWM 调速,以及小车前进/后退/左右转向的核心逻辑。
记录时间:2026年7月17日 22:06:25
审核修订:2026年7月18日 21:38:16(修正刹车/滑行概念、补充 JTAG 禁用及 PWM 模式说明,新增完整硬件引脚备份)
代码验证:所有代码已在实际硬件上编译烧录验证通过
ATmega16A 系列第六篇,希望本次学习记录能够帮到你,本文如有不足之处,欢迎留言指正。

一、前言:关于 L293D 电机驱动
L293D 电机驱动是智能小车的核心执行单元,也是寄存器 + 裸机练习的关键外设。我将初学时踩过的典型错误整理如下:
-
不了解 H 桥原理,IN 引脚电平搭配错乱,导致电机不转或只能单方向旋转;
-
混淆方向脚(IN)与调速使能脚(EN),错误地认为通过数字 IO 就能调速,结果电机全程满速;
-
PWM 寄存器配置错误,导致电机抖动、输出无力;
-
运动逻辑混乱,前进/后退/原地转向代码冗余且易出错。
本文将从硬件 H 桥底层原理出发,逐步过渡到寄存器级 PWM 调速,所有代码均匹配 HL-1/HL-2 小车硬件,并已在实物开发板上完成验证。
硬件原理图截图如下:

二、硬件接线(原理图 + 接线文档标准引脚)
2.1 L293D 对应 ATmega16 PC 口引脚
| L293D 引脚 | 单片机引脚 | 功能分类 |
|---|---|---|
| IN1 | PC7 | 左电机方向 1 |
| IN2 | PC6 | 左电机方向 2 |
| EN1 | PC5 | 左电机 PWM 调速使能(普通 IO 模式) |
| IN3 | PC3 | 右电机方向 1 |
| IN4 | PC2 | 右电机方向 2 |
| EN2 | PC4 | 右电机 PWM 调速使能(普通 IO 模式) |
注意:根据 HL-2 小车接线文档,右电机 IN3 = PC3、IN4 = PC2。实际使用时请以开发板 PCB 丝印标注为准。
2.2 完整外设引脚速查(HL-2 开发板 + HL-1 小车)
电机信号线(L293D 驱动,J3 接口)
| 小车底盘 J3 | 开发板引脚 | 说明 |
|---|---|---|
| IN1 | PC7 | 左电机方向 1 |
| IN2 | PC6 | 左电机方向 2 |
| EN1 | PC5 | 左电机使能(基础模式) |
| EN2 | PC4 | 右电机使能(基础模式) |
| IN3 | PC3 | 右电机方向 1 |
| IN4 | PC2 | 右电机方向 2 |
循迹/避障传感器(J5 接口)
| 车底盘 J5 | 开发板引脚 | 功能 |
|---|---|---|
| OUT1 | PB0 | 红外循迹(左) |
| OUT2 | PB1 | 红外循迹(右) |
| OUT3 | PA1 | 红外避障(左) |
| OUT4 | PA2 | 红外避障(右) |
| VCC | VCC | 5V 供电正极 |
| GND | GND | 5V 供电负极 |
超声波模块(HC-SR04,J6 接口,模块朝车头方向)
| 车底盘 J4 | 开发板引脚 | 功能 |
|---|---|---|
| IN5(Trig) | PB3 | 超声波触发信号 |
| IN6(Echo) | PB2 | 超声波回响信号 |
蓝牙串口模块(J7/J8 接口,注意正负极方向)
| 车底盘 J8 | 开发板引脚 | 功能 |
|---|---|---|
| IN7 | PD0(RXD) | 蓝牙接收(单片机串口 RX) |
| IN8 | PD1(TXD) | 蓝牙发送(单片机串口 TX) |
2.3 IO 初始化硬件规则
-
PC2~PC7 全部配置为输出;
-
IN1/IN2/IN3/IN4 为普通数字 IO,仅输出高低电平控制转向;
-
EN1/EN2 有两种使用方式:
-
基础方式:作为普通 IO 输出高电平(全速)或低电平(停止);
-
进阶方式:改用定时器 1 的 OC1A(PD5)和 OC1B(PD4)硬件 PWM 输出,实现无级调速,原 PC5/PC4 拉低禁用。
-
三、L293D H 桥底层原理
3.1 H 桥基础结构
一片 L293D 内部集成了两路独立的 H 桥,分别控制左、右两台直流电机。每路 H 桥由 4 个功率晶体管组成 “H” 型回路:
-
两个输入引脚(INx、INy)控制电流流向,决定电机的正转/反转;
-
EN 使能引脚相当于总开关:高电平时 H 桥导通,电机工作;低电平时切断电机供电,进入滑行停止状态(惯性自由停)。
核心安全红线:同一侧电机的两个 IN 引脚(如 IN1 与 IN2)绝对禁止同时为高电平! 否则会导致 H 桥上下管直通短路,芯片瞬间过热甚至烧毁冒烟。
3.2 单路电机电平真值表(以左电机 IN1/IN2 为例)
| EN1 | IN1(PC7) | IN2(PC6) | 左电机状态 | 物理实质 |
|---|---|---|---|---|
| 0 | X | X | 滑行停止 | 供电切断,电机依靠惯性自由转动 |
| 1 | 0 | 0 | 滑行停止 | H 桥输出高阻态,无制动力 |
| 1 | 1 | 0 | 正向旋转(前进) | 电流正向流过电机 |
| 1 | 0 | 1 | 反向旋转(后退) | 电流反向流过电机 |
| 1 | 1 | 1 | 禁止! | 上下管直通,芯片过流损坏 |
特别注意:L293D 本身不具备主动刹车功能。当 EN=1 且 IN1=IN2=0 时,H 桥处于高阻态,电机相当于开路滑行,并非"两端短路"制动。小车项目中如需急停,建议依靠机械结构或外部刹车电路,停车时统一使用
Car_Stop()使电机滑行停止。
3.3 EN 使能脚的作用
| EN 状态 | 效果 |
|---|---|
| 高电平(1) | H 桥通路打开,电机按 IN 电平转动(全速) |
| 低电平(0) | 切断电机供电,电机滑行停止(惯性停转) |
| PWM 方波 | 调节高电平占空比,改变电机平均电压,实现无级调速 |
四、基础定速电机代码(纯数字 IO,全速运行)
此方案适用于入门验证,PC5/PC4 作为普通 IO 输出固定高低电平,电机只能全速或停止。
4.1 电机 IO 初始化
VS code代码如下:
#define F_CPU 12000000UL
#include <avr/io.h>
#include <util/delay.h>
// 左电机
#define IN1 (1 << PC7)
#define IN2 (1 << PC6)
#define EN1 (1 << PC5)
// 右电机(按接线文档:IN3=PC3, IN4=PC2)
#define IN3 (1 << PC3)
#define IN4 (1 << PC2)
#define EN2 (1 << PC4)
void Motor_IO_Init(void)
{
// PC2~PC7 全部设为输出
DDRC |= IN1 | IN2 | EN1 | IN3 | IN4 | EN2;
// 初始全部拉低,电机滑行停止
PORTC &= ~(IN1 | IN2 | EN1 | IN3 | IN4 | EN2);
}
4.2 小车运动函数(前进/后退/左转/右转/停止)
c代码
// 小车全速前进
void Car_Forward(void)
{
PORTC |= IN1; // 左正转
PORTC &= ~IN2;
PORTC |= IN3; // 右正转
PORTC &= ~IN4;
PORTC |= EN1 | EN2; // 双电机使能全开
}
// 小车全速后退
void Car_Backward(void)
{
PORTC &= ~IN1;
PORTC |= IN2; // 左反转
PORTC &= ~IN3;
PORTC |= IN4; // 右反转
PORTC |= EN1 | EN2;
}
// 原地左转(左停 / 右前进)
void Car_Left(void)
{
PORTC &= ~(IN1 | IN2); // 左电机滑行停止
PORTC |= IN3;
PORTC &= ~IN4;
PORTC |= EN2; // 只使能右电机
PORTC &= ~EN1; // 左电机关闭
}
// 原地右转(右停 / 左前进)
void Car_Right(void)
{
PORTC |= IN1;
PORTC &= ~IN2;
PORTC &= ~(IN3 | IN4); // 右电机滑行停止
PORTC |= EN1; // 只使能左电机
PORTC &= ~EN2; // 右电机关闭
}
// 全车停止(滑行停止)
void Car_Stop(void)
{
PORTC &= ~(EN1 | EN2 | IN1 | IN2 | IN3 | IN4);
}
4.3 简易测试主函数
c代码
int main(void)
{
Motor_IO_Init();
while (1)
{
Car_Forward();
_delay_ms(1000);
Car_Left();
_delay_ms(800);
Car_Backward();
_delay_ms(1000);
Car_Right();
_delay_ms(800);
Car_Stop();
_delay_ms(500);
}
}
五、进阶:Timer1 硬件 PWM 无级调速方案(工程推荐)
5.1 设计思路
PC5/PC4 作为普通 IO 只能输出全速或停止,无法调速。本方案改用 Timer1 的硬件 PWM 输出(PD5/OC1A、PD4/OC1B) 作为使能调速端,IN 引脚仅控制方向,PWM 占空比(0~255)独立控制左右电机速度,非阻塞、无软件抖动。
引脚功能重映射:
-
方向引脚不变:IN1=PC7、IN2=PC6、IN3=PC3、IN4=PC2;
-
EN1 替换为 PD5(OC1A) —— 左电机 PWM 调速;
-
EN2 替换为 PD4(OC1B) —— 右电机 PWM 调速;
-
原 PC5/PC4 强制拉低,避免普通 IO 干扰 PWM 信号。
5.2 Timer1 PWM 初始化(快速 PWM 8 位模式)
⚠️ 关键前置操作:ATmega16A 默认使能 JTAG 接口,而 PD4(TDO)和 PD5(TMS)与 JTAG 复用。必须禁用 JTAG 才能正常使用这两个引脚作为 PWM 输出。
c代码
void Motor_PWM_Init(void)
{
// 【重要】禁用 JTAG,释放 PD4(OC1B) 和 PD5(OC1A)
// JTD 位需连续写入两次才能生效(数据手册要求)
MCUCSR |= (1 << JTD);
MCUCSR |= (1 << JTD);
// PD4(OC1B) 和 PD5(OC1A) 设为 PWM 输出
DDRD |= (1 << PD4) | (1 << PD5);
// 原 PC5/PC4(EN1/EN2)拉低,禁用普通 IO 干扰
PORTC &= ~((1 << PC4) | (1 << PC5));
// 配置定时器1为快速 PWM 模式5(8位,TOP=0x00FF)
// WGM10=1 (TCCR1A), WGM12=1 (TCCR1B) → 模式5
// COM1A1=1, COM1B1=1 → 非反转输出(OCx 在匹配时清零,TOP 时置位)
TCCR1A |= (1 << COM1A1) | (1 << COM1B1) | (1 << WGM10);
TCCR1B |= (1 << WGM12) | (1 << CS10); // 1 分频,PWM 频率 ≈ 46.875kHz
// 默认速度 70% 占空比(约 180/255)
OCR1A = 180;
OCR1B = 180;
}
// 设置左右电机速度(0~255)
void Set_Speed(uint8_t left, uint8_t right)
{
OCR1A = left;
OCR1B = right;
}
PWM 频率计算:
fPWM=fCPUN×(TOP+1)=12MHz1×256≈46.875kHzfPWM=N×(TOP+1)fCPU=1×25612MHz≈46.875kHz
该频率远高于人耳听觉范围,电机运行安静且无低频抖动。
5.3 带调速的运动函数(IN 控方向,PWM 控速度)
c代码
// 前进,左右速度独立可调
void Car_Forward_Speed(uint8_t l_spd, uint8_t r_spd)
{
Set_Speed(l_spd, r_spd);
PORTC |= IN1;
PORTC &= ~IN2;
PORTC |= IN3;
PORTC &= ~IN4;
}
// 后退
void Car_Backward_Speed(uint8_t l_spd, uint8_t r_spd)
{
Set_Speed(l_spd, r_spd);
PORTC &= ~IN1;
PORTC |= IN2;
PORTC &= ~IN3;
PORTC |= IN4;
}
// 原地左转(左停,右转)
// 注意:OCR1A=0 时 PWM 占空比为 0%,电机无驱动力;同时左 IN 已拉低,双重保险
void Car_Left_Speed(uint8_t r_spd)
{
Set_Speed(0, r_spd);
PORTC &= ~(IN1 | IN2); // 左电机方向清零(滑行)
PORTC |= IN3;
PORTC &= ~IN4;
}
// 原地右转(左转,右停)
void Car_Right_Speed(uint8_t l_spd)
{
Set_Speed(l_spd, 0);
PORTC |= IN1;
PORTC &= ~IN2;
PORTC &= ~(IN3 | IN4); // 右电机方向清零(滑行)
}
六、踩坑复盘(可能遇到的问题以及解决方案)
坑 1:IN1 与 IN2 同时置高,H 桥短路烧毁 L293D(这个问题一定要注意避免!!!)
| 项目 | 说明 |
|---|---|
| 故障现象 | 电机不转,驱动芯片迅速发烫,甚至冒烟、封装鼓包 |
| 根本原因 | IN1=1、IN2=1 时 H 桥上下管同时导通,电源直接对地短路,大电流烧毁芯片 |
| 解决方案 | 在所有运动函数中强制检查:同一电机的两个 IN 绝对不可同时为 1。刹车/停止统一置 0 |
坑 2:混淆 IN 与 EN,用数字 IO 调速出现的问题记录
| 项目 | 说明 |
|---|---|
| 故障现象 | 修改 PC5/PC4 电平,电机速度无变化,只有转/停两种状态 |
| 根本原因 | PC5/PC4 只是普通数字 IO,输出恒定高低电平,无 PWM 波形,无法改变平均电压 |
| 解决方案 | 使用 Timer1 的 OC1A/OC1B(PD5/PD4)硬件 PWM 作为使能调速端。原 PC5/PC4 必须拉低禁用,防止电平冲突 |
坑 3:电机转向左右相反,小车无法直线行驶
| 项目 | 说明 |
|---|---|
| 故障现象 | 调用 Car_Forward() 时,小车原地打转或走弧线,左右轮一正一反 |
| 根本原因 | IN1/IN2 电平逻辑与实际电机接线相反;或左右电机在底盘上安装方向镜像 |
| 排查与解决 | 单独测试单侧电机:IN1=1, IN2=0 为正转,IN1=0, IN2=1 为反转。确认单侧逻辑正确后,再组合运动函数。若接反,交换代码中的 IN1/IN2 或 IN3/IN4 定义即可 |
坑 4:_delay_ms() 阻塞导致整车响应卡顿
| 项目 | 说明 |
|---|---|
| 故障现象 | 执行转向延时期间,按键无响应、循迹传感器数据无法读取,小车动作生硬 |
| 根本原因 | _delay_ms() 为阻塞式延时,CPU 在此期间无法处理其他外设 |
| 解决方案 | 入门实验阶段:使用 _delay_ms() 演示运动逻辑完全可接受,简单直观。完整工程项目:建议结合 Timer0/Timer2 时基中断 + 有限状态机(FSM)实现非阻塞延时 |
七、关键开发规范(整车项目强制完成)
-
严格禁止同一侧电机的 IN1 和 IN2 同时拉高,代码 review 时需重点检查;
-
整车优先使用 Timer1 硬件 PWM 调速,不要用软件延时或 IO 翻转模拟 PWM(浪费 CPU 且占空比不稳定);
-
调试前先调用
Car_Stop()使电机滑行停止,再修改转向电平,避免 H 桥状态跳变时产生冲击电流; -
左右电机速度尽量匹配,因制造公差,可微调左右速度值(如左 180、右 185)补偿电机差异,实现直线行驶;
-
L293D 驱动芯片在大电流下温升明显,长时间满速运行建议加装小型散热片,并确保底盘通风良好;
-
电机启动瞬间电流较大,可能导致单片机复位,需在电源端并联 100μF 以上电解电容进行滤波稳压;
-
使用 PD4/PD5 做 PWM 输出前,务必在代码中禁用 JTAG(
MCUCSR |= (1<<JTD);连续写两次),否则 PWM 功能无效。
八、配套 Makefile(已在实际项目中验证)
makefile代码
# =====================================================
# Makefile 用于 ATmega16A 智能小车项目
# =====================================================
# ----- 项目配置 -----
# 目标文件名(生成 main.hex 和 main.elf)
TARGET = atmega16a
# 单片机型号(必须小写!)
MCU = atmega16
# 晶振频率(12MHz)
F_CPU = 12000000UL
# 源文件(如果有多个 .c 文件,用空格隔开,例如 main.c uart.c)
SRC = main.c
# ----- 工具链设置(使用绝对路径,解决 as 冲突)-----
# 注意:请根据你实际的工具链安装路径修改下面这行!
AVR_BIN = E:/AVR-GCC/avr8-gnu-toolchain-win32_x86-master/bin
# 告诉系统用哪个编译器
CC = $(AVR_BIN)/avr-gcc
# 用来将 elf 转 hex 的工具
OBJCOPY = $(AVR_BIN)/avr-objcopy
# 测量并报告编译完成程序占用空间的大小
SIZE = $(AVR_BIN)/avr-size
# ----- 编译选项 -----
# -mmcu: 指定芯片
# -DF_CPU: 定义时钟频率
# -Os:优化代码大小
# -Wall: 开启所有警告(方便查错)
# -B: 强制指定工具链搜索路径(防止调用系统 as 汇编器)【关键修改点】
CFLAGS = -mmcu=$(MCU) -DF_CPU=$(F_CPU) -Os -Wall -B$(AVR_BIN)/..
# 链接选项(同样需要 -B 参数)
LDFLAGS = -mmcu=$(MCU) -B$(AVR_BIN)/..
# ----- 自动推导的中间文件 -----
# 将 .c 文件替换为 .o 文件(例如 main.c → main.o)
OBJ = $(SRC:.c=.o)
# 汇编输出文件(用于调试,make asm 生成)
ASM = $(SRC:.c=.s)
# =====================================================
# 目标规则
# =====================================================
# 默认目标:输入 make 时执行的操作
all: $(TARGET).hex
@echo "=========================================="
@echo "编译完成!"
@echo "烧录请执行:make flash"
@echo "清理请执行:make clean"
@echo "查看汇编:make asm"
@echo "=========================================="
# 编译:将 .c 文件编译成 .elf(可执行文件)
# 使用 $^ 表示所有依赖(.c 文件),$@ 表示目标文件
$(TARGET).elf: $(SRC)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
@echo "编译完成:$@"
# 生成烧录文件:将 .elf 文件转换成 .hex(十六进制文件)
$(TARGET).hex: $(TARGET).elf
$(OBJCOPY) -O ihex $< $@
@echo "生成烧录文件:$@"
$(SIZE) $(TARGET).elf
# ----- 辅助目标 -----
# 查看程序占用空间(不重新编译)
size:
$(SIZE) $(TARGET).elf
# 生成汇编文件(用于调试优化,查看编译器生成的汇编指令)
asm: $(SRC)
$(CC) $(CFLAGS) -S $< -o $(ASM)
@echo "汇编文件已生成:$(ASM)"
# 编译并生成 .o 文件(不链接,用于检查语法)
build: $(OBJ)
@echo "目标文件已生成:$(OBJ)"
# ----- 烧录目标 -----
# 烧录:(先安装 avrdude 并连接 USBASP)
# -c usbasp:指定下载器类型
# -p $(MCU):指定芯片型号
# -U flash:w:$(TARGET).hex:写入 flash
flash: all
avrdude -c usbasp -p $(MCU) -U flash:w:$(TARGET).hex
# 烧录并显示详细信息(调试用)
flash-verbose: all
avrdude -c usbasp -p $(MCU) -U flash:w:$(TARGET).hex -v -v
# 读取 flash 内容备份(读出当前芯片中的程序)
read-flash:
avrdude -c usbasp -p $(MCU) -U flash:r:backup.hex:i
# 读取熔丝位(查看当前熔丝配置,谨慎操作)
read-fuse:
avrdude -c usbasp -p $(MCU) -U lfuse:r:-:h -U hfuse:r:-:h
# ----- 清理目标 -----
# 清理:删除编译生成的临时文件(Windows 兼容写法)
# 注意:-if exist 前面的 '-' 表示忽略错误,即使文件不存在也不报错
clean:
-if exist $(OBJ) del $(OBJ)
-if exist $(TARGET).elf del $(TARGET).elf
-if exist $(TARGET).hex del $(TARGET).hex
-if exist $(ASM) del $(ASM)
@echo "已清理 .o .elf .hex .s 文件"
# 深度清理:删除所有临时文件和备份文件
distclean: clean
-if exist backup.hex del backup.hex
@echo "深度清理完成"
# 声明伪目标(防止与同名文件冲突)
.PHONY: all clean flash size asm build read-flash read-fuse flash-verbose distclean
附录一:完整实验代码
附录 1.1 基础全速模式(纯数字 IO)
c代码如下:
/*
* =====================================================
* 实验名称:L293D 电机驱动基础测试(全速运行)
* 硬件平台:HL-2 开发板 + HL-1 小车底盘
* 芯片:ATmega16A
* 晶振:12MHz
* 功能:前进1秒 → 左转0.8秒 → 后退1秒 → 右转0.8秒 → 停止0.5秒 循环
* 验证状态:✅ 已在实际硬件上验证通过
* =====================================================
*/
#define F_CPU 12000000UL
#include <avr/io.h>
#include <util/delay.h>
// ===== 左电机 =====
#define IN1 (1 << PC7)
#define IN2 (1 << PC6)
#define EN1 (1 << PC5)
// ===== 右电机(按接线文档:IN3=PC3, IN4=PC2)=====
#define IN3 (1 << PC3)
#define IN4 (1 << PC2)
#define EN2 (1 << PC4)
// ===== 电机IO初始化 =====
void Motor_IO_Init(void)
{
// PC2~PC7 全部设为输出
DDRC |= IN1 | IN2 | EN1 | IN3 | IN4 | EN2;
// 初始全部拉低,电机滑行停止
PORTC &= ~(IN1 | IN2 | EN1 | IN3 | IN4 | EN2);
}
// ===== 运动函数 =====
// 小车全速前进
void Car_Forward(void)
{
PORTC |= IN1; // 左正转
PORTC &= ~IN2;
PORTC |= IN3; // 右正转
PORTC &= ~IN4;
PORTC |= EN1 | EN2; // 双电机使能全开
}
// 小车全速后退
void Car_Backward(void)
{
PORTC &= ~IN1;
PORTC |= IN2; // 左反转
PORTC &= ~IN3;
PORTC |= IN4; // 右反转
PORTC |= EN1 | EN2;
}
// 原地左转(左停 / 右前进)
void Car_Left(void)
{
PORTC &= ~(IN1 | IN2); // 左电机滑行停止
PORTC |= IN3;
PORTC &= ~IN4;
PORTC |= EN2; // 只使能右电机
PORTC &= ~EN1; // 左电机关闭
}
// 原地右转(右停 / 左前进)
void Car_Right(void)
{
PORTC |= IN1;
PORTC &= ~IN2;
PORTC &= ~(IN3 | IN4); // 右电机滑行停止
PORTC |= EN1; // 只使能左电机
PORTC &= ~EN2; // 右电机关闭
}
// 全车停止(滑行停止)
void Car_Stop(void)
{
PORTC &= ~(EN1 | EN2 | IN1 | IN2 | IN3 | IN4);
}
// ===== 主函数 =====
int main(void)
{
Motor_IO_Init();
while (1)
{
Car_Forward();
_delay_ms(1000);
Car_Left();
_delay_ms(800);
Car_Backward();
_delay_ms(1000);
Car_Right();
_delay_ms(800);
Car_Stop();
_delay_ms(500);
}
}
编译成功截图如下:

附录 1.2 PWM 无级调速模式(工程推荐)
c代码如下:
/*
* =====================================================
* 实验名称:L293D 电机驱动 PWM 调速测试
* 硬件平台:HL-2 开发板 + HL-1 小车底盘
* 芯片:ATmega16A
* 晶振:12MHz
* 功能:使用 Timer1 硬件 PWM 无级调速
* 注意:PD4/PD5 与 JTAG 复用,必须在代码中禁用 JTAG
* 验证状态:✅ 已在实际硬件上验证通过
* =====================================================
*/
#define F_CPU 12000000UL
#include <avr/io.h>
#include <util/delay.h>
// ===== 左电机(方向引脚不变)=====
#define IN1 (1 << PC7)
#define IN2 (1 << PC6)
// ===== 右电机(方向引脚不变)=====
#define IN3 (1 << PC3)
#define IN4 (1 << PC2)
// ===== PWM 调速引脚(替换原 EN1/EN2)=====
// EN1 → PD5 (OC1A) 左电机 PWM
// EN2 → PD4 (OC1B) 右电机 PWM
// ===== 电机方向 IO 初始化 =====
void Motor_Direction_Init(void)
{
// PC2~PC7 方向引脚设为输出(PC5/PC4 原 EN 不再使用)
DDRC |= IN1 | IN2 | IN3 | IN4;
// PC5/PC4 原 EN 引脚拉低,禁用干扰
PORTC &= ~((1 << PC4) | (1 << PC5));
// 方向引脚初始拉低
PORTC &= ~(IN1 | IN2 | IN3 | IN4);
}
// ===== PWM 初始化(Timer1 快速 PWM 模式5,8位)=====
void Motor_PWM_Init(void)
{
// 【关键】禁用 JTAG,释放 PD4(OC1B) 和 PD5(OC1A)
// JTD 位需连续写入两次才能生效(ATmega16A 数据手册要求)
MCUCSR |= (1 << JTD);
MCUCSR |= (1 << JTD);
// PD4(OC1B) 和 PD5(OC1A) 设为 PWM 输出
DDRD |= (1 << PD4) | (1 << PD5);
// 配置定时器1为快速 PWM 模式5(8位,TOP=0x00FF)
// WGM10=1 (TCCR1A), WGM12=1 (TCCR1B) → 模式5
// COM1A1=1, COM1B1=1 → 非反转输出
TCCR1A |= (1 << COM1A1) | (1 << COM1B1) | (1 << WGM10);
TCCR1B |= (1 << WGM12) | (1 << CS10); // 1 分频
// 默认速度 70% 占空比(180/255)
OCR1A = 180;
OCR1B = 180;
}
// ===== 设置左右电机速度(0~255)=====
void Set_Speed(uint8_t left, uint8_t right)
{
OCR1A = left;
OCR1B = right;
}
// ===== 带调速的运动函数 =====
// 前进,左右速度独立可调
void Car_Forward_Speed(uint8_t l_spd, uint8_t r_spd)
{
Set_Speed(l_spd, r_spd);
PORTC |= IN1;
PORTC &= ~IN2;
PORTC |= IN3;
PORTC &= ~IN4;
}
// 后退
void Car_Backward_Speed(uint8_t l_spd, uint8_t r_spd)
{
Set_Speed(l_spd, r_spd);
PORTC &= ~IN1;
PORTC |= IN2;
PORTC &= ~IN3;
PORTC |= IN4;
}
// 原地左转(左停,右转)
void Car_Left_Speed(uint8_t r_spd)
{
Set_Speed(0, r_spd);
PORTC &= ~(IN1 | IN2);
PORTC |= IN3;
PORTC &= ~IN4;
}
// 原地右转(左转,右停)
void Car_Right_Speed(uint8_t l_spd)
{
Set_Speed(l_spd, 0);
PORTC |= IN1;
PORTC &= ~IN2;
PORTC &= ~(IN3 | IN4);
}
// 停止(滑行停止)
void Car_Stop(void)
{
Set_Speed(0, 0);
PORTC &= ~(IN1 | IN2 | IN3 | IN4);
}
// ===== 主函数 =====
int main(void)
{
Motor_Direction_Init();
Motor_PWM_Init();
while (1)
{
// 前进:左 80% 速度,右 75% 速度(微调补偿电机差异)
Car_Forward_Speed(204, 191);
_delay_ms(2000);
// 左转:左停,右 50% 速度
Car_Left_Speed(128);
_delay_ms(1000);
// 后退:左右 60% 速度
Car_Backward_Speed(153, 153);
_delay_ms(2000);
// 右转:左 50% 速度,右停
Car_Right_Speed(128);
_delay_ms(1000);
// 停止
Car_Stop();
_delay_ms(1000);
}
}
编译截图如下:

附录二:实验现象记录
附录 2.1 基础全速模式实验现象
| 时间段 | 执行函数 | 左轮状态 | 右轮状态 | 小车运动 | 验证状态 |
|---|---|---|---|---|---|
| 0~1s | Car_Forward() | 正转(全速) | 正转(全速) | 直线前进 | 正常 |
| 1~1.8s | Car_Left() | 停止(滑行) | 正转(全速) | 原地左转 | 正常 |
| 1.8~2.8s | Car_Backward() | 反转(全速) | 反转(全速) | 直线后退 | 正常 |
| 2.8~3.6s | Car_Right() | 正转(全速) | 停止(滑行) | 原地右转 | 正常 |
| 3.6~4.1s | Car_Stop() | 停止 | 停止 | 滑行停止 | 正常 |
实测现象描述:
-
小车前进/后退直线度良好,无明显跑偏;
-
原地转向干脆利落,转弯半径约等于车身宽度;
-
L293D 芯片在连续运行 5 分钟后有明显温升(约 45°C),手触温热;
-
停止时因惯性滑行约 2~3cm 后完全静止。
附录 2.2 PWM 调速模式实验现象
| 时间段 | 执行函数 | 左轮速度 | 右轮速度 | 小车运动 | 验证状态 |
|---|---|---|---|---|---|
| 0~2s | Car_Forward_Speed(204, 191) | 80%(204) | 75%(191) | 直线前进 | 正常 |
| 2~3s | Car_Left_Speed(128) | 0%(停止) | 50%(128) | 缓慢左转 | 正常 |
| 3~5s | Car_Backward_Speed(153, 153) | 60%(153) | 60%(153) | 直线后退 | 正常 |
| 5~6s | Car_Right_Speed(128) | 50%(128) | 0%(停止) | 缓慢右转 | 正常 |
| 6~7s | Car_Stop() | 0% | 0% | 滑行停止 | 正常 |
实测现象描述:
-
80% 速度时电机噪音明显低于全速模式,运行更平稳;
-
50% 速度转向平滑,无顿挫感;
-
PWM 频率 46.875kHz 下电机无 audible 啸叫;
-
左右速度微调补偿(204 vs 191)有效修正了电机个体差异导致的跑偏;
-
实测 7.4V 供电下,80% 速度约等于全速的 70% 实际转速(电机启动电压阈值导致)。
附录 2.3 编译烧录验证结果
编译输出:
text
PS E:\001\ATmega16A> make
avr-gcc -mmcu=atmega16 -DF_CPU=12000000UL -Os -Wall -BE:/AVR-GCC/... -c main.c -o main.o
avr-gcc -mmcu=atmega16 -BE:/AVR-GCC/... -o atmega16a.elf main.o
avr-objcopy -O ihex atmega16a.elf atmega16a.hex
AVR Memory Usage
----------------
Device: atmega16
Program: 314 bytes (1.9% Full)
(.text + .data + .bootloader)
Data: 0 bytes (0.0% Full)
(.data + .bss + .noinit)
==========================================
编译完成!
烧录请执行:make flash
清理请执行:make clean
查看汇编:make asm
==========================================
输出因为汉字的原因会导致输出乱码,但不会影响编译文件的正常使用
烧录输出:
text
PS E:\001\ATmega16A> make flash
avrdude -c usbasp -p atmega16 -U flash:w:atmega16a.hex
avrdude: AVR device initialized and ready to accept instructions
avrdude: device signature = 0x1e9403 (probably atmega16)
avrdude: reading input file "atmega16a.hex"
avrdude: writing flash (314 bytes):
avrdude: 314 bytes of flash written
avrdude: safemode: Fuses OK (E:FF, H:D9, L:FF)
avrdude: done. Thank you.
附录三:HL-2 开发板 + HL-1 小车完整硬件引脚备份
附录 3.1 主控芯片基础引脚(ATmega16A-PU,40PDIP)
电源与基准
| 引脚 | 名称 | 功能 |
|---|---|---|
| 10 | VCC | 5V 数字主电源 |
| 11 | GND | 数字地 |
| 32 | AVCC | ADC 模拟电源(板载直连 VCC) |
| 31 | AREF | ADC 外部参考电压引脚(引出排针) |
晶振/复位
| 引脚 | 名称 | 板载硬件 |
|---|---|---|
| 12 | XTAL2 | 12MHz 晶振输出 |
| 13 | XTAL1 | 12MHz 晶振输入,配套 22pF 电容 |
| 9 | RESET | 10K 上拉复位按键,ISP/JTAG 共用复位 |
附录 3.2 PORTA(PA0PA7,3340 脚,ADC 模拟输入)
| 引脚 | 芯片脚 | 复用外设 | 开发板外接模块 |
|---|---|---|---|
| PA0 | 40 | ADC0 | 预留排针 |
| PA1 | 39 | ADC1 | HL-1 小车避障传感器 OUT3 |
| PA2 | 38 | ADC2 | HL-1 小车避障传感器 OUT4 |
| PA3 | 37 | ADC3 | 蜂鸣器 FM 输出 |
| PA4 | 36 | ADC4 | LCD1602/LCD12864 RS |
| PA5 | 35 | ADC5 | LCD1602/LCD12864 RW |
| PA6 | 34 | ADC6 | LCD1602/LCD12864 E |
| PA7 | 33 | ADC7 | 预留排针 |
附录 3.3 PORTB(PB0PB7,18 脚,定时器/SPI/计数)
| 引脚 | 芯片脚 | 片内复用 | 开发板/小车外设 |
|---|---|---|---|
| PB0 | 1 | T0 | HL-1 循迹 OUT1;数码管 E 段 |
| PB1 | 2 | T1 | HL-1 循迹 OUT2;数码管 A 段 |
| PB2 | 3 | INT2/ICP1 | HC-SR04 超声波 Echo;数码管 D 段 |
| PB3 | 4 | OC0/AIN1 | HC-SR04 超声波 Trig;数码管 F 段 |
| PB4 | 5 | SS | 数码管 CS4、G 段 |
| PB5 | 6 | MOSI | ISP-MOSI;流水灯 L0;数码管 C;DS1302 时钟 |
| PB6 | 7 | MISO | ISP-MISO;DS1302 数据;数码管 B |
| PB7 | 8 | SCK | ISP-SCK;DS1302 复位;数码管 H |
PB 配套外设速查:
| 外设 | 引脚分配 |
|---|---|
| DS1302 RTC | PB5(CLK)、PB6(DATA)、PB7(RST) |
| 8 位数码管段选 | PB1(A)/PB7(B)/PB2(D)/PB0(E)/PB3(F)/PB6(G)/PB4(H) |
| 数码管位选 | CS1~CS4:PB5/PB0/PB3/PB4 |
| 流水灯 L0 | PB5 |
| SPI ISP 下载 | PB5(MOSI)、PB6(MISO)、PB7(SCK) |
附录 3.4 PORTC(PC0PC7,2431 脚,I2C/JTAG/电机驱动)
| 引脚 | 芯片脚 | 片内复用 | 开发板/小车功能 |
|---|---|---|---|
| PC0 | 24 | SCL | AT24C02 时钟;12864 串口 SCL |
| PC1 | 25 | SDA | AT24C02 数据;12864 串口 SDA |
| PC2 | 26 | TCK(JTAG) | JTAG 时钟;小车 IN4(右电机方向) |
| PC3 | 27 | TMS(JTAG) | JTAG 模式;小车 IN3(右电机方向) |
| PC4 | 28 | TDO(JTAG) | JTAG 输出;小车 EN2(右电机使能) |
| PC5 | 29 | TDI(JTAG) | JTAG 输入;小车 IN2(左电机方向) |
| PC6 | 30 | TOSC1 | 无 32K 晶振;小车 IN1 |
| PC7 | 31 | TOSC2 | 无 32K 晶振;小车 EN1 |
注意:PC2/PC3 与电机 IN4/IN3 的对应关系请以开发板丝印为准。本文按 HL-2 官方接线文档:IN3=PC3、IN4=PC2。
附录 3.5 PORTD(PD0PD7,1421 脚,串口/外部中断/按键)
| 引脚 | 芯片脚 | 片内复用 | 开发板外设 |
|---|---|---|---|
| PD0 | 14 | RXD | 串口接收;蓝牙 IN7;独立按键 S1 |
| PD1 | 15 | TXD | 串口发送;蓝牙 IN8;矩阵行 |
| PD2 | 16 | INT0 | 独立按键 K1(10K 上拉);矩阵行 |
| PD3 | 17 | INT1 | 独立按键 K2(10K 上拉);矩阵行 |
| PD4 | 18 | OC1B | 板载三极管 Q1;PWM 调速 EN2 替换端 |
| PD5 | 19 | OC1A | 预留;PWM 调速 EN1 替换端 |
| PD6 | 20 | ICP1 | 1838 红外接收;矩阵列;独立 S3 |
| PD7 | 21 | OC2 | 矩阵列;独立 S4 |
PD 配套外设速查:
| 外设 | 引脚分配 |
|---|---|
| 独立按键 | K1=PD2(INT0)、K2=PD3(INT1),外置 10K 上拉 |
| 2×4 矩阵键盘 | 行 PD1/PD2/PD3;列 PD6/PD7 |
| 蓝牙串口 | IN7=PD0(RXD)、IN8=PD1(TXD) |
| 红外接收 1838 | PD6 |
| MAX232 RS232 | PD0(RX)、PD1(TX) |
附录 3.6 板载独立功能模块完整清单
| 类别 | 模块 | 引脚分配 |
|---|---|---|
| 存储 | AT24C02 (I2C) | PC0(SCL)、PC1(SDA) |
| 存储 | DS1302 RTC | PB5/PB6/PB7,自带 32.768K + 纽扣电池 |
| 显示 | LCD1602 8 位并口 | PA4(RS)、PA5(RW)、PA6(E),PB0~PB7 数据 |
| 显示 | LCD12864 | 并口同 1602;串口模式 PC0/PC1 |
| 显示 | 8 位数码管 | PB 全端口驱动 |
| 显示 | 流水灯 L0 | PB5 |
| 输入 | 独立按键 ×2 | PD2、PD3(外部上拉) |
| 输入 | 2×4 矩阵键盘 | 行 PD1/PD2/PD3;列 PD6/PD7 |
| 输入 | PS/2 键盘 | PB5(CLK)、PB7(DATA)、PB6(RST) |
| 输入 | 1838 红外接收 | PD6 |
| 通信 | ISP 下载 | PB5/MOSI、PB6/MISO、PB7/SCK、RESET |
| 通信 | JTAG 调试 | PC2/TCK、PC3/TMS、PC4/TDO、PC5/TDI |
| 通信 | 硬件 USART | PD0(RX)、PD1(TX),MAX232 DB9 |
| 通信 | TWI(I2C) | PC0、PC1 |
| 声音 | 蜂鸣器 | PA3(三极管驱动) |
| 时钟 | 主晶振 | 12MHz XTAL1/XTAL2 |
| 电源 | USB 5V | 电源开关、指示灯 L8、复位 10K 上拉 |
附录 3.7 硬件关键注意事项
-
PD2/PD3 独立按键外置 10K 上拉,代码必须关闭 PORTD 内部上拉,否则电平识别异常;
-
PC6/PC7(TOSC1/TOSC2) 无板载 32.768K 晶振,无法直接使用 Timer2 异步低功耗模式;
-
JTAG 开启时 PC2~PC5 被强制占用,使用这些引脚做普通 IO 前需
MCUCSR |= (1<<JTD);连续写两次禁用 JTAG; -
LCD1602/LCD12864 使用 PA4/5/6,此时 PA 口 ADC 功能被占用;
-
电机 EN1/EN2(PC5/PC4)推荐改用 OC1A/OC1B(PD5/PD4)PWM 调速;
-
ISP 下载占用 PB5/PB6/PB7,下载时不要操作 SPI 外设;
-
AT24C02 和 12864 串口共用 PC0/PC1 I2C 总线,多设备注意地址区分。
附录四:实操验证清单
在实物硬件上验证代码前,请逐项确认:
硬件检查清单
-
小车四轮已架空(垫书本或支架),避免电机堵转;
-
L293D 的 VS(Vcc2)供电电压在 5V~12V 范围内;
-
电源端已并联 100μF 电解电容 + 0.1μF 陶瓷电容;
-
L293D 芯片表面已贴小型散热片(长时间运行建议);
-
准备手指感知温度,如芯片迅速发烫立即断电。
代码编译检查清单
-
确认
Car_Forward()中 IN1/IN2 不同时为高; -
确认
Car_Backward()中 IN1/IN2 不同时为高; -
PWM 模式确认
MCUCSR |= (1<<JTD);已连续写入两次; -
使用
make编译通过,无报错。
首次上电步骤
-
不接电机,先烧录代码;
-
用万用表测量关键引脚电平:
-
PC7/PC6/PC3/PC2 方向引脚是否符合预期;
-
PD5/PD4(PWM 模式)是否有 PWM 方波信号;
-
-
确认无误后,断电,接上电机;
-
重新上电,手指轻触 L293D 监控温度;
-
观察车轮运动是否与附录 2.1/2.2 一致。
十、全文总结
-
硬件层面:L293D 内置两路独立 H 桥,IN 引脚控制转向,EN 使能控制通断与调速;单侧 IN 同时为高是绝对红线,必须杜绝;
-
基础方案:PC 口全部作为数字 IO,全速定速运动,适合入门学习和硬件验证;
-
工程方案:使用 ATmega16 的 Timer1 硬件 PWM(PD4/PD5)作为 EN 调速端,实现 0~255 无级变速,适配循迹小车、遥控小车等场景,切记禁用 JTAG 以释放 PD4/PD5;
-
运动逻辑:前进/后退/原地转向的代码抽象可一键移植至整车主循环,配合外部中断按键、ADC 循迹、超声波测距、蓝牙遥控即可搭建完整自动小车系统;
-
代码验证:所有代码已在 HL-2 开发板 + HL-1 小车底盘实物上完成编译、烧录、运行验证,附录中记录了完整的实验现象。
希望本文能帮助你绕开 L293D 使用中的常见陷阱,快速实现稳定可靠的小车运动控制。如有疑问或发现遗漏,欢迎在评论区留言讨论!
配套资源建议:结合 ATmega16A 数据手册第 12 章(定时器)和第 14 章(端口)阅读,理解 PWM 波形生成和 IO 寄存器操作的底层细节,会有更深收获。
到此我的第六坑文章填坑就完成了,希望对你有所帮助,不足之处希望能被留言指出。

记录时间:2026年7月18日21:37:51
&spm=1001.2101.3001.5002&articleId=162939853&d=1&t=3&u=6cbef86195014a67a7c47c00d4aa1355)
959

被折叠的 条评论
为什么被折叠?



