第四章:裸机编程基础——寄存器直接操作、GPIO控制LED闪烁、UART串口打印Hello World
好,咱们进入正题。这一章是嵌入式开发的「起手式」。说白了,就是让芯片听你的话,让它亮灯就亮灯,让它说话就说话。
很多初学者一上来就怼操作系统、怼复杂框架,结果连个LED都点不亮。我个人习惯是,拿到一块新芯片,第一件事就是写裸机程序。为什么?因为只有裸机才能让你真正理解芯片的「脾气」。
4.1 寄存器直接操作——芯片的「开关面板」
芯片内部其实就是一个巨大的开关矩阵。每个开关叫「寄存器」,你拨动开关,芯片就干活。
拿三星的Exynos系列来说,它的GPIO控制器有一堆寄存器。比如控制方向的寄存器叫GPIOx_CON,控制输出电平的叫GPIOx_DAT。你往这些寄存器里写值,就等于在拨开关。
举个例子,假设我们要控制GPIOA的第5号引脚:
// 定义寄存器地址(以Exynos4412为例)
#define GPIOA_BASE 0x11400000
#define GPIOA_CON (*(volatile unsigned int *)(GPIOA_BASE + 0x00))
#define GPIOA_DAT (*(volatile unsigned int *)(GPIOA_BASE + 0x04))
// 设置第5脚为输出模式(假设每4位控制一个引脚)
GPIOA_CON = (GPIOA_CON & ~(0xF << 20)) | (0x1 << 20);
// 输出高电平
GPIOA_DAT |= (1 << 5);
这里有个关键点:volatile关键字。我刚开始学的时候漏写过,结果编译器优化后,寄存器操作全被跳过了。嗯,那LED死活不亮,查了半天才发现是这个问题。
volatile 修饰。否则编译器可能把寄存器访问优化掉,导致硬件不响应。
4.2 GPIO控制LED闪烁——让芯片「眨眼」
LED闪烁是嵌入式界的「Hello World」。别看简单,里面门道不少。
硬件连接:LED正极接GPIO引脚,负极通过电阻接地。这样引脚输出高电平时LED亮,低电平时灭。
软件实现:
void delay(volatile unsigned int count) {
while(count--);
}
void led_blink(void) {
// 初始化:设置GPIO为输出
GPIOA_CON = (GPIOA_CON & ~(0xF << 20)) | (0x1 << 20);
while(1) {
GPIOA_DAT |= (1 << 5); // 亮
delay(0x100000); // 延时
GPIOA_DAT &= ~(1 << 5); // 灭
delay(0x100000); // 延时
}
}
这里有个坑:延时函数不能太精确。为什么?因为裸机没有定时器中断,纯靠CPU空转。我在项目中遇到过,用这种延时做呼吸灯效果,结果不同芯片频率不同,效果完全不一样。后来改用硬件定时器才解决。
4.3 UART串口打印Hello World——让芯片「说话」
LED闪烁只能告诉你「芯片活着」,但没法告诉你「芯片在想什么」。这时候就需要串口了。
UART通信说白了就是两根线:TX(发送)和RX(接收)。波特率要一致,数据格式要匹配。我见过最坑的事,就是两个人调试串口,一个设9600,一个设115200,结果互相看不懂对方在说什么。
三星芯片的UART寄存器配置:
// UART寄存器定义(以Exynos4412 UART0为例)
#define UART0_BASE 0x13800000
#define ULCON0 (*(volatile unsigned int *)(UART0_BASE + 0x00))
#define UCON0 (*(volatile unsigned int *)(UART0_BASE + 0x04))
#define UTRSTAT0 (*(volatile unsigned int *)(UART0_BASE + 0x10))
#define UTXH0 (*(volatile unsigned int *)(UART0_BASE + 0x20))
#define URXH0 (*(volatile unsigned int *)(UART0_BASE + 0x24))
#define UBRDIV0 (*(volatile unsigned int *)(UART0_BASE + 0x28))
void uart_init(void) {
// 设置波特率:115200 @ 50MHz时钟
UBRDIV0 = 27; // 计算值:50MHz / (16 * 115200) - 1 ≈ 26.1,取整27
// 配置:8位数据,1位停止位,无校验
ULCON0 = 0x03;
// 配置:使能发送和接收,使用中断模式
UCON0 = 0x05;
}
void uart_putc(char c) {
// 等待发送缓冲区空
while(!(UTRSTAT0 & 0x02));
UTXH0 = c;
}
void uart_puts(const char *str) {
while(*str) {
uart_putc(*str++);
}
}
// 主函数
void main(void) {
uart_init();
uart_puts("Hello World!\r\n");
while(1) {
// 循环等待
}
}
这里要注意波特率计算。公式是:UBRDIV = (时钟频率 / (16 × 目标波特率)) - 1。我刚开始做的时候,算出来的值直接取整,结果串口数据全是乱码。后来才发现要四舍五入,而且还要考虑时钟误差。
核心要点:
- 寄存器操作是裸机编程的基础,必须理解地址映射和位操作
- GPIO控制LED是验证硬件是否正常的最快方法
- UART串口是嵌入式调试的「眼睛」,学会它等于学会和芯片对话
4.4 实战经验总结
我做了这么多年嵌入式,总结出几条铁律:
- 先看数据手册:别急着写代码。把寄存器地址、位定义搞清楚,能省一半调试时间。
- 从简单开始:先点灯,再串口,最后才上复杂外设。一步一个脚印。
- 善用调试工具:示波器、逻辑分析仪、串口助手,这些工具比你的眼睛靠谱得多。
- 注意代码可读性:用宏定义代替魔法数字,用函数封装重复操作。不然三个月后你自己都看不懂写的什么。
我曾经接手过一个项目,前任工程师把所有寄存器操作都写在main函数里,整整2000行。我花了三天才理清楚每个位是干什么的。从那以后,我坚持用结构体封装寄存器组,用宏定义位掩码。代码清晰了,bug也少了。
好了,这一章就到这里。下一章我们会讲中断和定时器,到时候LED闪烁就不用傻傻的延时函数了。