目录
文章传送门
一、什么是串口
二、本项目串口的FPGA实现
三、串口驱动程序的编写
四、上板测试
文章传送门
开发一个RISC-V上的操作系统(一)—— 环境搭建_riscv开发环境_Patarw_Li的博客-CSDN博客
开发一个RISC-V上的操作系统(二)—— 系统引导程序(Bootloader)_Patarw_Li的博客-CSDN博客
开发一个RISC-V上的操作系统(三)—— 串口驱动程序(UART)_Patarw_Li的博客-CSDN博客
一、什么是串口
串口(UART)又名异步收发传输器(Universal Asynchronous Receiver/Transmitter),是一种通用的数据通信协议,也是异步串行通信口(串口)的总称,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将串行数据转换成并行数据。SPI和I2C为同步通信接口,双方时钟频率相同,而UART属于异步通信接口,没有统一时钟,靠起始位和终止位来接收数据。
串口包括RS232、RS499、RS423等接口标准规范,我们这里使用的是RS232:
上图为串口的通信方式,可以同时收发(全双工通信)。其中rx负责接收,tx负责发送,每次发送10bit数据(起始位+8bit数据+停止位),从最低位开始发送。
二、本项目串口的FPGA实现
在写串口的驱动程序之前,我们首先要知道如何与开发板上的串口进行交互,所以我们要先看看我们riscv cpu项目是怎么实现串口模块的。
项目仓库:
cpu_prj: 一个基于RISC-V指令集的CPU实现
串口模块的实现在 FPGA/rtl/perips/目录下的uart.v文件中,它作为一个外设挂载在rib总线上:
在总线模块 FPGA/rtl/core/rib.v中可以看到, uart.v外设的地址范围为0x1000_0000 ~ 0x1fff_ffff,用于访问uart模块中的寄存器(实际uart模块只有三个寄存器,不需要这么大空间,但是影响不大):
下面是uart.v的代码。其中uart_rx和uart_tx引脚为串口接收和发送引脚;最下面五个信号则用于读写串口寄存器。
// 串口模块,默认波特率为9600
module uart(
input wire clk ,
input wire rst_n ,
input wire uart_rx , // uart接收引脚
output reg uart_tx , // uart发送引脚
input wire wr_en_i , // uart寄存器写使能信号
input wire[`INST_ADDR_BUS] wr_addr_i , // uart寄存器写地址
input wire[`INST_DATA_BUS] wr_data_i , // uart写数据
input wire[`INST_ADDR_BUS] rd_addr_i , // uart寄存器读地址
output reg [`INST_DATA_BUS] rd_data_o // uart读数据
);
// 寄存器地址定义
parameter UART_CTRL = 4'd0,
UART_TX_DATA_BUF= 4'd4,
UART_RX_DATA_BUF= 4'd8;
// addr: 0x0
// 低两位(1:0)为TI和RI
// TI:发送完成位,该位在数据发送完成时被设置为高电平
// RI:接收完成位,该位在数据接收完成时被设置为高电平
reg[31:0] uart_ctrl;
// addr: 0x4
// 发送数据寄存器
reg[31:0] uart_tx_data_buf;
// addr: 0x8
// 接收数据寄存器
reg[31:0] uart_rx_data_buf;
parameter BAUD_CNT_MAX = `CLK_FREQ / `UART_BPS;
parameter IDLE = 4'd0,
BEGIN= 4'd1,
RX_BYTE = 4'd2,
TX_BYTE = 4'd3,
END = 4'd4;
wire uart_rx_temp;
reg uart_rx_delay; // rx延迟后的输入
reg[3:0] uart_rx_state; // rx状态机
reg[12:0] rx_baud_cnt; // rx计数器
reg[3:0] rx_bit_cnt; // rx比特计数
reg[3:0] uart_tx_state; // rx状态机
reg[12:0] tx_baud_cnt; // rx计数器
reg[3:0] tx_bit_cnt; // rx比特计数
reg tx_data_rd; // 发送数据就绪信号
// 读写寄存器,write before read
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_ctrl <= `ZERO_WORD;
uart_tx_data_buf <= `ZERO_WORD;
end
else begin
if(wr_en_i == 1'b1) begin
case(wr_addr_i[3:0])
UART_CTRL: begin
uart_ctrl = wr_data_i;
end
UART_TX_DATA_BUF: begin
uart_tx_data_buf = wr_data_i;
end
default: begin
end
endcase
end
if(uart_tx_state == END && tx_baud_cnt == 1) begin
uart_ctrl[1] = 1'b1; // TI置1,代表发送完毕,需要软件置0
end
if(uart_rx_state == END && rx_baud_cnt == 1) begin
uart_ctrl[0] = 1'b1; // RI置1,代表接收完毕,需要软件置0
end
case(rd_addr_i[3:0])
UART_CTRL: begin
rd_data_o = uart_ctrl;
end
UART_TX_DATA_BUF: begin
rd_data_o = uart_tx_data_buf;
end
UART_RX_DATA_BUF: begin
rd_data_o = uart_rx_data_buf;
end
default: begin
rd_data_o = `ZERO_WORD;
end
endcase
end
end
/* TX发送模块 */
// tx数据就绪信号 tx_data_rd
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_data_rd <= 1'b0;
end
else if(wr_en_i == 1'b1 && wr_addr_i[3:0] == UART_TX_DATA_BUF) begin
tx_data_rd <= 1'b1;
end
else if(uart_tx_state == END && tx_baud_cnt == 1) begin
tx_data_rd <= 1'b0;
end
else begin
tx_data_rd <= tx_data_rd;
end
end
// tx_baud_cnt计数
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_baud_cnt <= 13'd0;
end
else if(uart_tx_state == IDLE || tx_baud_cnt == BAUD_CNT_MAX - 1) begin
tx_baud_cnt <= 13'd0;
end
else begin
tx_baud_cnt <= tx_baud_cnt + 1'b1;
end
end
// TX发送模块
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_tx_state <= IDLE;
tx_bit_cnt <= 4'd0;
uart_tx <= 1'b1;
end
else begin
case(uart_tx_state)
IDLE: begin
uart_tx <= 1'b1;
if(tx_data_rd == 1'b1) begin
uart_tx_state <= BEGIN;
end
else begin
uart_tx_state <= uart_tx_state;
end
end
BEGIN: begin
uart_tx <= 1'b0;
if(tx_baud_cnt == BAUD_CNT_MAX - 1) begin
uart_tx_state <= TX_BYTE;
end
else begin
uart_tx_state <= uart_tx_state;
end
end
TX_BYTE: begin
if(tx_bit_cnt == 4'd7 && tx_baud_cnt == BAUD_CNT_MAX - 1) begin
tx_bit_cnt <= 4'd0;
uart_tx_state <= END;
end
else if(tx_baud_cnt == BAUD_CNT_MAX - 1) begin
tx_bit_cnt <= tx_bit_cnt + 1'b1;
end
else begin
uart_tx <= uart_tx_data_buf[tx_bit_cnt];
end
end
END: begin
uart_tx <= 1'b1;
if(tx_baud_cnt == BAUD_CNT_MAX - 1) begin
uart_tx_state <= IDLE;
end
else begin
uart_tx_state <= uart_tx_state;
end
end
default: begin
uart_tx_state <= IDLE;
tx_bit_cnt <= 4'd0;
uart_tx <= 1'b1;
end
endcase
end
end
/* RX接收模块 */
// 将输入rx延迟4个时钟周期,减少亚稳态的影响
delay_buffer #(
.DEPTH(4),
.DATA_WIDTH(1)
) u_delay_buffer(
.clk (clk), // Master Clock
.data_i (uart_rx), // Data Input
.data_o (uart_rx_temp) // Data Output
);
always @ (posedge clk) begin
uart_rx_delay <= uart_rx_temp;
end
// rx_baud_cnt计数
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rx_baud_cnt <= 13'd0;
end
else if(uart_rx_state == IDLE || rx_baud_cnt == BAUD_CNT_MAX - 1) begin
rx_baud_cnt <= 13'd0;
end
else begin
rx_baud_cnt <= rx_baud_cnt + 1'b1;
end
end
// RX接收模块
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_rx_state <= IDLE;
rx_bit_cnt <= 4'd0;
uart_rx_data_buf <= `ZERO_WORD;
end
else begin
case(uart_rx_state)
IDLE: begin
if(uart_rx_temp == 1'b0 && uart_rx_delay == 1'b1) begin
uart_rx_state <= BEGIN;
end
else begin
uart_rx_state <= uart_rx_state;
end
end
BEGIN: begin
if(rx_baud_cnt == BAUD_CNT_MAX - 1) begin
uart_rx_state <= RX_BYTE;
end
else begin
uart_rx_state <= uart_rx_state;
end
end
RX_BYTE: begin
if(rx_bit_cnt == 4'd7 && rx_baud_cnt == BAUD_CNT_MAX - 1) begin
rx_bit_cnt <= 4'd0;
uart_rx_state <= END;
end
else if(rx_baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
uart_rx_data_buf[rx_bit_cnt] <= uart_rx_delay;
end
else if(rx_baud_cnt == BAUD_CNT_MAX - 1) begin
rx_bit_cnt <= rx_bit_cnt + 1'b1;
end
else begin
uart_rx_state <= uart_rx_state;
end
end
END: begin
if(rx_baud_cnt == 1) begin
uart_rx_state <= IDLE;
end
else begin
uart_rx_state <= uart_rx_state;
end
end
default: begin
uart_rx_state <= IDLE;
rx_bit_cnt <= 4'd0;
uart_rx_data_buf <= `ZERO_WORD;
end
endcase
end
end
endmodule
在串口模块uart.v中定义了三个寄存器,分别为 串口控制寄存器 uart_ctrl、 串口发送数据缓存寄存器 uart_tx_data_buf、 串口接收数据缓存寄存器 uart_rx_data_buf,它们的作用分别是:
- uart_ctrl:串口控制寄存器,只有最低两位有效,分别为TI和RI。TI,发送完成标志位,该位在数据发送完成时被设置为1;RI,接收完成标志位,该位在数据接收完成时被设置为高电平。在数据发送/接收完成后由uart模块将TI/RI置1,这样驱动程序就可以通过读取该寄存器的TI/RI位来确定数据是否发送/接收完成,这两位需要软件复位。
- uart_tx_data_buf:串口发送数据缓存寄存器,只要uart_tx_data_buf寄存器有数据写入,就开始发送数据,发送完毕将TI置1。
- uart_rx_data_buf:串口接收数据缓存寄存器,用于接收用户通过rx端口发送的数据,接收完就将RI置1。
它们的地址偏移分别是0,4,8,实际的物理地址为0x10000000,0x10000004,0x10000008:
在了解上述内容后,我们就可以开始编写串口驱动程序了。
三、串口驱动程序的编写
源码放在我的gitee仓库,欢迎star:
riscv_os: 一个RISC-V上的简易操作系统
代码在 01_UART 目录下,目录结构为:
inc目录存放头文件,其中platform.h里定义了串口设备首地址UART:
uart.c即为我们的串口驱动程序,代码内容如下:
#define UART_REG_ADDRESS(reg) ((uint8_t *) (UART + reg))
/*
* UART registers map
*/
#define UART_CTRL 0
#define UART_TX_DATA_BUF 4
#define UART_RX_DATA_BUF 8
#define uart_read_reg(reg) (*(UART_REG_ADDRESS(reg)))
#define uart_write_reg(reg, data) (*(UART_REG_ADDRESS(reg)) = (data))
void uart_init()
{
// init uart_ctrl reg to 0
uart_write_reg(UART_CTRL, 0x00);
}
void uart_putc(char ch)
{
// fill send buf
uart_write_reg(UART_TX_DATA_BUF, ch);
// wait send over
while((uart_read_reg(UART_CTRL) & (1 << 1)) != (1 << 1)){}
// set TI to 0
uart_write_reg(UART_CTRL, (uart_read_reg(UART_CTRL) & ~(1 << 1)));
}
void uart_puts(char *s)
{
while(*s){
uart_putc(*s++);
}
}
char uart_getc()
{
// wait RI to 1
while((uart_read_reg(UART_CTRL) & (1 << 0)) != (1 << 0)){}
// set RI to 0
uart_write_reg(UART_CTRL, (uart_read_reg(UART_CTRL) & ~(1 << 0)));
// read receive buf
return uart_read_reg(UART_RX_DATA_BUF);
}
void uart_gets(char *s, uint8_t len){
uint8_t i = 0;
while(i < len - 1 && (*s = uart_getc()) != '\n'){
++i;
++s;
}
*(s + 1) = '\0';
}
其中第一行定义的宏UART_REG_ADDRESS(reg)用于取得对应寄存器reg的地址,下面三个宏UART_CTRL,UART_TX_DATA_BUF,UART_RX_DATA_BUF 即为寄存器的偏移地址,例如uart_ctrl寄存器的地址为 (UART + UART_CTRL ) = 0x10000000 + 0。
宏函数uart_read_reg(reg)用于读取对应reg的内容,uart_write_reg(reg, data)则用于将data写入对应reg。
uart_init()函数很简单,用于初始化uart_ctrl寄存器。
uart_putc(char ch)函数用于发送一个字符给上位机。首先会把要发的内容写入uart_tx_data_buf;写入后串口模块会自动发送数据,发送完成后TI会置1,在此期间该函数会一直轮询查看TI是否置1,若TI置1则代表发送完成。发送完成后需要将TI置0。
void uart_putc(char ch)
{
// fill send buf
uart_write_reg(UART_TX_DATA_BUF, ch);
// wait send over
while((uart_read_reg(UART_CTRL) & (1 << 1)) != (1 << 1)){}
// set TI to 0
uart_write_reg(UART_CTRL, (uart_read_reg(UART_CTRL) & ~(1 << 1)));
}
uart_getc()函数用于接收上位机发过来的一个字符。因为只有RI为1才代表有一个字节数据需要接收,所以在RI为0的时候需要一直循环等待;待RI为1后,先把RI复位(置0),然后再读取uart_rx_data_buf的内容,得到上位机发过来的一个字符数据。
char uart_getc()
{
// wait RI to 1
while((uart_read_reg(UART_CTRL) & (1 << 0)) != (1 << 0)){}
// set RI to 0
uart_write_reg(UART_CTRL, (uart_read_reg(UART_CTRL) & ~(1 << 0)));
// read receive buf
return uart_read_reg(UART_RX_DATA_BUF);
}
uart_puts()和uart_gets()则用于连续获取一组字符得到一个字符串,是通过连续调用上面两个函数实现的。
至此,uart驱动程序的内容已经讲解完毕,接下来调用试试看。
四、上板测试
首先我们看看主程序里面的内容:
void start_kernel(void){
/* User code begin */
printf("Hello %d", 110);
printf("World!\n");
char msg[20] = "";
while(1){
uart_gets(msg, 20);
uart_puts(msg);
}
/* User code end */
while(1){}; // stop here!
}
可以看到这是一个串口回环,会把我们上位机发送的msg又回传到上位机。接下来我们烧录到板子上看看,在烧录前确保你的板子已经把我的riscv cpu跑起来了,可以看我前面的文章。
首先执行make得到os.bin文件,然后通过python烧录程序把os.bin烧录到处理器的memory中,烧录完后我们需要用串口工具来调试,可以下载我上传到cpu_prj仓库里面的串口工具:
打开串口调试工具,设置好参数后点击右下角的打开按钮:
打开串口后,按下复位键即可看到串口输出内容 ,输出的是printf函数打印的内容(pirntf函数也调用了uart_puts函数,这里就不细讲了,感兴趣的话可以去看printf.c文件的内容):
在下面的输入框内输入内容,最后加一个回车(内容最后一定要加一个回车,uart_gets()函数是通过回车符号来判断内容的结束的),然后点击发送即可看到上面显示你发送的内容:
至此,串口驱动程序实验结束,实现了串口,我们之后开发调试都会方便很多~
遇到问题欢迎加群 892873718 交流~