目录
一、内存映射
1、什么是内存映射?为什么要内存映射?
2、gec6818如何进行内存映射?(相关的函数)
3、内存映射代码 :
二、模拟IIC的底层代码实现
1、配置输入输出方向
2、拉高拉低引脚
3、获得总线传回的数据
4、协议的实现
三、TM1650的使用
1、TM1650简介
2、TM1650怎么驱动?
2.1 控制命令
2.2 显存地址
2.3 对显存地址写数据时序
四、针对TM1650实现的IIC驱动
1、TM1650模块驱动(完整驱动代码)
2、驱动测试程序
3、Makefile
4、Linux操作
关键词:
1、模拟IIC协议
2、S5P6818
3、ARM内存访问控制
4、TM1650数码管
本片博客记录在gec6818平台上的模拟IIC实现,并成功驱动 TM1650 数码管。
模拟IIC也可适用其他所有用IIC协议驱动的外设!
如文章有错误,请各位不吝赐教。
一、内存映射
1、什么是内存映射?为什么要内存映射?
外设的SFR(特殊功能寄存器)编址与内存的编址是同一个地址空间,叫做IO内存。
但是当内核想要访问某些特殊寄存器或者内存单元的时候,不能直接使用IO内存的物理地址进行访问,而要通过该物理地址所对应的虚拟地址进行访问,因此,必须要进行地址的转换,而从物理地址到虚拟地址的映射我们就称为内存映射。
2、gec6818如何进行内存映射?(相关的函数)
//struct resource *
#include <linux/ioport.h>
//ioremap iounmap 之类的函数
#include <linux/io.h>
/*
ioremap函数
1、参数:
offset:要映射的物理内存区的起始地址
size:物理地址的范围
2、返回值:
虚拟地址的指针
3、头文件引用:
#include <linux/io.h>
*/
static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size);
/*
iounmap函数
1、参数:
io_addr:虚拟地址的指针
2、返回值:
无
3、头文件引用:
#include <linux/io.h>
*/
void __iounmap(volatile void __iomem *io_addr);
3、内存映射代码 :
//1、定义SCL、SDA的GPIO虚拟内存指针
static void __iomem * IIC_OUT_VA; //0x00
static void __iomem * IIC_OUTENB_VA; //0x04
static void __iomem * IIC_ALTFN0_VA; //0x20
static void __iomem * IIC_ALTFN1_VA; //0x24
static void __iomem * IIC_PAD_VA; //0x18
//1、定义tm1650设备结构体
struct miscdevice tm1650_IIC_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "tm1650_drv",
.fops = &tm1650_IIC_fops,
};
//2、IO内存的动态映射,由物理地址得到虚拟地址
//这里用到 GPIOC7 GPIOC8 两个引脚 查阅GPIOC的基址
IIC_OUT_VA = ioremap(0xc001c000, 0x1000);
if(IIC_OUT_VA == NULL){
printk("ioremap error\n");
misc_deregister(&tm1650_IIC_misc);
return -EBUSY;
}
//3、得到每个寄存器的虚拟地址(根据s5p6818文档查阅)
IIC_OUTENB_VA = IIC_OUT_VA + 0x04; //0x04
IIC_ALTFN0_VA = IIC_OUT_VA + 0x20; //0x20
IIC_ALTFN1_VA = IIC_OUT_VA + 0x24; //0x24
IIC_PAD_VA = IIC_OUT_VA + 0x18; //0x18
下面讲解上述的代码以及如何在文档查阅物理地址和偏移量:
1)三星s5p6818文档的这个部分是讲GPIO控制器的,相关寄存器解说就在这里。
2) 查看寄存器描述,可以知道基址(Base Address)以及各寄存器的偏移(Offset),当然大家也可以看后面的描述(Description)来了解一下各寄存器的作用是什么。
3)举例来说,譬如我实验的IIC引脚用的GPIO是GPIOC,那么配置输出模式的寄存器GPIOCOUT基址就为 0xc001c000 ,偏移就为 0x1000。
所以也就可以看到内存映射代码的ioremap函数参数(上面)写的是:
IIC_OUT_VA = ioremap(0xc001c000, 0x1000);
同理,我们想找到 GPIOCOUTENB 寄存器的物理地址也就很简单了,也就是:
基址+偏移:
GPIOCOUTENB = 0xc001c000 + 0x004;
但是!!我们不用都去使用基址+偏移找到其物理地址,再用ioremap进行映射获得虚拟地址给内核使用,因为我们可以发现:
每个GPIOXOUT寄存器偏移都为 0x1000 ,后面的寄存器(譬如GPIOXENB......等)偏移都在 0x1000内(GPIOXENB的偏移是 0x004),也就是说我们可以根据GPIOXOUT寄存器的首地址来找到其他寄存器,这个“首地址”可以是物理地址,也可以是ioremap后的虚拟地址,偏移量都生效。所以明白了这点之后,我们就不用对每个寄存器进行ioremap来获得虚拟地址,我们只需要获得GPIOXOUT的虚拟地址,然后用该虚拟地址加上偏移量就可以获得其他寄存器的虚拟地址!也就说我们只使用了一次ioremap,巧妙地使用了偏移量。
因此就可以看到上面内存映射代码:
//得到每个寄存器的虚拟地址
IIC_OUTENB_VA = IIC_OUT_VA + 0x04; //0x04
IIC_ALTFN0_VA = IIC_OUT_VA + 0x20; //0x20
IIC_ALTFN1_VA = IIC_OUT_VA + 0x24; //0x24
IIC_PAD_VA = IIC_OUT_VA + 0x18; //0x18
总结:至此,内存映射我们就完成了,我们找到了相关寄存器的虚拟地址,我们就可以使用这些虚拟地址进行读写以控制IO,来实现IIC。
二、模拟IIC的底层代码实现
模拟IIC通信将涉及到SDA、SCL的
- 配置输入输出方向
- 拉高拉低
- 获得总线传回的数据
- 协议的实现(起始、结束信号等)
1、配置输入输出方向
主要是SDA的方向需要配置,SCL只是时钟线
void SDA_OUT(void){ (*(unsigned int *)IIC_OUTENB_VA) |= (1<<8); } void SDA_IN(void){ (*(unsigned int *)IIC_OUTENB_VA) &= ~(1<<8); }
可以看到我们操作的是上面我们通过内存映射获得的虚拟地址(IIC_OUTENB_VA),通过对这个虚拟地址的读写来对寄存器进行读写,达到控制GPIO的作用。
2、拉高拉低引脚
SCL
void SCL_set0(void){ (*(unsigned int *)IIC_OUT_VA) &= ~(1<<7); } void SCL_set1(void){ (*(unsigned int *)IIC_OUT_VA) |= (1<<7); }
SDA
void SDA_set0(void){ (*(unsigned int *)IIC_OUT_VA) &= ~(1<<8); } void SDA_set1(void){ (*(unsigned int *)IIC_OUT_VA) |= (1<<8); }
3、获得总线传回的数据
char SDA_VAL(void){
return ((*(unsigned int *)IIC_PAD_VA) >> 8)&0X01;
}
4、协议的实现
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
SDA_set1();
SCL_set1();
udelay(2);
SDA_set0();//START:when CLK is high,DATA change form high to low
udelay(2);
SCL_set0();//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
SCL_set0();
SDA_set0();//STOP:when CLK is high DATA change form low to high
udelay(2);
SCL_set1();
SDA_set1();//发送I2C总线结束信号
udelay(2);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
SDA_set1();udelay(2);
SCL_set1();udelay(2);
while(SDA_VAL())//
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
SCL_set0();//时钟输出0
return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
SCL_set0();
SDA_OUT();
SDA_set0();
udelay(2);
SCL_set1();
udelay(2);
SCL_set0();
}
//不产生ACK应答
void IIC_NAck(void)
{
SCL_set0();//SCL = 0
SDA_OUT();//SDA设为输出
SDA_set1();//SDA置高平
udelay(2);
SCL_set1();
udelay(2);
SCL_set0();
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
SCL_set0();//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if(((txd&0x80)>>7) == 1)
SDA_set1();
else if(((txd&0x80)>>7) == 0)
SDA_set0();
txd<<=1;
SCL_set1();
udelay(2);
SCL_set0();
udelay(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
SCL_set0();
udelay(2);
SCL_set1();
receive<<=1;
if(SDA_VAL())receive++;
udelay(2);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
总结:至此我们就实现了模拟IIC,根据相关外设的IIC通信时序的要求,我们就可以实现与外设的IIC通信。下面我们就来实现IIC设备 TM1650 控制。
三、TM1650的使用
1、TM1650简介
2、TM1650怎么驱动?
2.1 控制命令
我们只用模式命令来显示数码管 0x48
2.2 显存地址
写LED显示数据的时候,按照从显示地址从高位到低位,从数据字节的高位到低位操作。
1)DIG1~DIG4 :数码管第一位到第四位。
2)显存地址 :0x68 0x6A 0x6C 0x6E
2.3 对显存地址写数据时序
1)显示数据码表 NUM[10]:
/* TM1650 码表*/ const uint8_t NUM[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
2)根据文档时序,封装写字节函数 TM1650_Wr_RAM(uint8_t Address, uint8_t Data):
/* TM1650 写字节函数 */ void TM1650_Wr_RAM(uint8_t Address, uint8_t Data) { IIC_Start(); IIC_Send_Byte(Address); IIC_Wait_Ack(); IIC_Send_Byte(Data); IIC_Wait_Ack(); IIC_Stop(); }
3)往显存写数据显示数据,同样我们封装成函数 set_number(char mode, int num):
/* TM1650 写入数字函数 第一位:num / 1000 第二位:小数点. 第三位:num % 100 / 10 --十位 第四位:num % 10 --个位 */ void set_number(char mode, int num) { TM1650_Wr_RAM(0x68, NUM[num / 1000]); if(mode == 1) { TM1650_Wr_RAM(0x6A, 0X80);// 第二位只显示小数点 } else { TM1650_Wr_RAM(0x6A, NUM[num % 1000 / 100]);// 第二位显示正常数据 } TM1650_Wr_RAM(0x6C, NUM[num % 100 / 10]); TM1650_Wr_RAM(0x6E, NUM[num % 10]); }
4)打开显示设置命令:
//初始化TM1650模块 TM1650_Wr_RAM(0x48, 0x71);
四、针对TM1650实现的IIC驱动
1、TM1650模块驱动(完整驱动代码)
下面是源文件:tm1650_drv.c
使用 insmod 插入模块 tm1650_drv.ko:
[root@GEC6818 /IOT/tangJW/a53_10]#insmod tm1650_drv
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <mach/platform.h>
#include <linux/errno.h>
#include <linux/ioport.h> //struct resource *
#include <linux/io.h> //ioremap
#include <linux/uaccess.h> //copy_from_user
#include <linux/gpio.h>
#include <linux/delay.h>
//定义SCL、SDA的GPIO虚拟内存指针
static void __iomem * IIC_OUT_VA; //0x00
static void __iomem * IIC_OUTENB_VA; //0x04
static void __iomem * IIC_ALTFN0_VA; //0x20
static void __iomem * IIC_ALTFN1_VA; //0x24
static void __iomem * IIC_PAD_VA; //0x18
void SDA_OUT(void){
(*(unsigned int *)IIC_OUTENB_VA) |= (1<<8);
}
void SDA_IN(void){
(*(unsigned int *)IIC_OUTENB_VA) &= ~(1<<8);
}
void SDA_set0(void){
(*(unsigned int *)IIC_OUT_VA) &= ~(1<<8);
}
void SDA_set1(void){
(*(unsigned int *)IIC_OUT_VA) |= (1<<8);
}
char SDA_VAL(void){
return ((*(unsigned int *)IIC_PAD_VA) >> 8)&0X01;
}
void SCL_set0(void){
(*(unsigned int *)IIC_OUT_VA) &= ~(1<<7);
}
void SCL_set1(void){
(*(unsigned int *)IIC_OUT_VA) |= (1<<7);
}
//模拟IIC协议用到的函数
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
SDA_set1();
SCL_set1();
udelay(2);
SDA_set0();//START:when CLK is high,DATA change form high to low
udelay(2);
SCL_set0();//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
SCL_set0();
SDA_set0();//STOP:when CLK is high DATA change form low to high
udelay(2);
SCL_set1();
SDA_set1();//发送I2C总线结束信号
udelay(2);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
SDA_set1();udelay(2);
SCL_set1();udelay(2);
while(SDA_VAL())//
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
SCL_set0();//时钟输出0
return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
SCL_set0();
SDA_OUT();
SDA_set0();
udelay(2);
SCL_set1();
udelay(2);
SCL_set0();
}
//不产生ACK应答
void IIC_NAck(void)
{
SCL_set0();//SCL = 0
SDA_OUT();//SDA设为输出
SDA_set1();//SDA置高平
udelay(2);
SCL_set1();
udelay(2);
SCL_set0();
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
SCL_set0();//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if(((txd&0x80)>>7) == 1)
SDA_set1();
else if(((txd&0x80)>>7) == 0)
SDA_set0();
txd<<=1;
SCL_set1();
udelay(2);
SCL_set0();
udelay(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
SCL_set0();
udelay(2);
SCL_set1();
receive<<=1;
if(SDA_VAL())receive++;
udelay(2);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
//------------- TM1650 @TangJW ---------------
/* TM1650 码表*/
const uint8_t NUM[10]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
/* TM1650 写字节函数 */
void TM1650_Wr_RAM(uint8_t Address, uint8_t Data)
{
IIC_Start();
IIC_Send_Byte(Address);
IIC_Wait_Ack();
IIC_Send_Byte(Data);
IIC_Wait_Ack();
IIC_Stop();
}
/*
TM1650 写入数字函数
第一位:num / 1000
第二位:小数点.
第三位:num % 100 / 10 --十位
第四位:num % 10 --个位
*/
void set_number(char mode, int num)
{
TM1650_Wr_RAM(0x68, NUM[num / 1000]);
if(mode == 1)
{
TM1650_Wr_RAM(0x6A, 0X80);// 第二位只显示小数点
}
else
{
TM1650_Wr_RAM(0x6A, NUM[num % 1000 / 100]);// 第二位显示正常数据
}
TM1650_Wr_RAM(0x6C, NUM[num % 100 / 10]);
TM1650_Wr_RAM(0x6E, NUM[num % 10]);
}
// 定义字符设备的文件操作集
/*
open函数:
初始化TM1650模块
*/
int TM1650_open(struct inode *inode, struct file *filp)
{
//初始化TM1650模块
TM1650_Wr_RAM(0x48, 0x71);
printk("TM1650_init! \n");
return 0;
}
/*
ioctl函数:实现set_number
cmd:数据参数
args:
--0 第二位显示正常数据
--1 第二位显示小数点
*/
static void TM1650_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
set_number(args, cmd);
}
//定义文件操作集
const struct file_operations tm1650_IIC_fops = {
.owner = THIS_MODULE,
.open = TM1650_open,
.unlocked_ioctl = TM1650_ioctl,
};
struct miscdevice tm1650_IIC_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "tm1650_drv",
.fops = &tm1650_IIC_fops,
};
/*
TM1650初始化
-SCL: GPIOC 7
-SDA: GPIOC 8
*/
static int __init TM1650_init(void)
{
int ret;
printk("TM1650_init... \n");
//注册混杂设备
ret = misc_register(&tm1650_IIC_misc);
if(ret < 0)
{
printk("misc_register error\n");
return -EBUSY;
}
//IO内存的动态映射,由物理地址得到虚拟地址 GPIOC7 GPIOC8
IIC_OUT_VA = ioremap(0xc001c000, 0x1000);
if(IIC_OUT_VA == NULL){
printk("ioremap error\n");
misc_deregister(&tm1650_IIC_misc);
return -EBUSY;
}
//得到每个寄存器的虚拟地址
IIC_OUTENB_VA = IIC_OUT_VA + 0x04; //0x04
IIC_ALTFN0_VA = IIC_OUT_VA + 0x20; //0x20
IIC_ALTFN1_VA = IIC_OUT_VA + 0x24; //0x24
IIC_PAD_VA = IIC_OUT_VA + 0x18; //0x18
//访问虚拟地址:GPIOC7 SCL GPIOC8 SDA
//将引脚设置成普通的GPIO
*(unsigned int *)IIC_ALTFN0_VA &=~((3<<14) + (3<<16));
*(unsigned int *)IIC_ALTFN0_VA |=((1<<14) + (1<<16));
//将2个GPIO设置为输出
*(unsigned int *)IIC_OUTENB_VA |= ((1<<7)+(1<<8));
//2个GPIO输出高电平,IIC默认是释放模式
*(unsigned int *)IIC_OUT_VA |= ((1<<7)+(1<<8));
return ret;
}
static void __exit TM1650_exit(void)
{
iounmap(IIC_OUT_VA);
misc_deregister(&tm1650_IIC_misc);
}
module_init(TM1650_init);
module_exit(TM1650_exit);
MODULE_AUTHOR("tangjingwei@qq.com");
MODULE_DESCRIPTION("IIC Device /TM1650/ Driver @TangJW");
MODULE_LICENSE("GPL");
2、驱动测试程序
下面是源文件:tm1650_test.c
测试程序运行:
1、第二个参数为 1,控制第二位总是显示小数点
[root@GEC6818 /IOT/tangJW/a53_10]#./tm1650_test 2023 1
2、第二个参数为 0,控制第二位显示正常数据
[root@GEC6818 /IOT/tangJW/a53_10]#./tm1650_test 2023 0
/*****************************
TM1650测试函数,请配合 tm1650_drv.ko 使用
作者:唐京伟
联系:tangjingwei@qq.com
IIC测试硬件引脚:
-SCL: GPIOC 7
-SDA: GPIOC 8
*****************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int num, mode;
/* 1、调用设备的open函数(即发送初始化tm1650的iic时序信号)*/
int fd = open("/dev/tm1650_drv",O_RDWR);
if(fd<0){
perror("open /dev/tm1650_drv failed! ");
return 0;
}
/* 2、调用设备的ioctl函数(即发送控制显示tm1650的iic时序信号)*/
num = ((int)(argv[1][0]) - 48)*1000 + ((int)(argv[1][1]) - 48)*100 + ((int)(argv[1][2]) - 48)*10 + ((int)(argv[1][3]) - 48);
mode = (int)(argv[2][0]) - 48;
printf("Open TM1650 successfully, check it out!\n");
//mode: 1-第二位显示小数点 0-第二位显示正常数据
ioctl(fd, num, mode);
while(1)
{
}
}
3、Makefile
下面是文件:Makefile
Makefile 注意点:
1、obj-m 应为 tm1650_drv.o
2、KERNELDIR 应为 自己的Linux环境下的6818源码kernel目录路径
3、CROSS_COMPILE 应为 自己的Linux环境下的交叉编译器路径
4、命令介绍
make 命令:使用交叉编译器编译 tm1650_drv.c
make test 命令:使用arm-linux-gcc编译 tm1650_test.c
obj-m:=tm1650_drv.o
KERNELDIR:=/usr/local/6818GEC/kernel
CROSS_COMPILE:=/usr/local/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD:=$(shell pwd)
default:
$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(PWD) modules
test:
arm-linux-gcc tm1650_test.c -o tm1650_test
clean:
rm -rf *.o *.order .*.cmd *.mod.c *.symvers
4、Linux操作
编译驱动:
编译测试程序:
上传文件至开发板,我用的是SecureCRT,并插入模块驱动:
文章来源:https://www.toymoban.com/news/detail-528598.html
相关文档链接:
三星S5P6818官方文档 + TM1650官方文档 提取码:2023文章来源地址https://www.toymoban.com/news/detail-528598.html
到了这里,关于ARM开发之基于IIC协议的TM1650驱动实现(模拟IIC实现)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!