转载一个网上资料---electrlife网友的资料,觉得不错,发一下!
单片机编程之 - 程序模块化及可复用性(一)
关于这个贴子的原由请查看http://www.amobbs.com/thread-5580903-1-1.html
另外申明:技术只是实现产品的一种工具,因此你应该花更多的时间去关注产品的本身!
当你接触单片机编程时,我想模块化,可复用,面象对象你应该都听说过吧!对于像我们这些
电工出生的来说,那些所谓的软件工程、设计模式等可能比较晦涩难懂,甚至是天书,至少对于我
来说是这样的!这里我想讲的模块化,和可复用性和纯软件工程是不可比的,不是一个数量级,
呵呵!我只说说我对这些的理解!因为模块化和可复用性就我个人而言,目前认为是一意思,也即
模块化是为了最大限度的可复用,而可复用的代码的实现方法之一就是模块化,哈哈有点绕啊!
在谈模块化和可复用性之前,先假设一下应用环境,因为这几年写程序都是基于RTOS模式来
写的,对于RTOS我这里多说两句,有些工程师可能不太愿意使用RTOS,他们觉得裸机程序更加效率、
写好了也确实很优雅,但就我个人而言,我是强烈建议学习和理解RTOS的,就算你不使用,RTOS的
一些思想也是可以帮助对编程有更深一步的认识,因此以后谈及的所有程序代码也都会考虑在RTOS下
运行所需要的互斥处理,即临界段问题。如果你是裸机编程那也没关系,因为理解临界段问题对
你来说也不是什么坏事!后面我会在“单片机编程之-RTOS临界段处理” 中再详细说下临界段的问题!
再者为了抓住重点,强调思想与结构,因此叙述中所出现的代码将不会作为详细的注解。
如果你接触过C++或类似面向对象的语言的话,你会面向对象这个概念无孔不入,就连单片机编程的书籍
也会提到或是介绍。面向对象是一个很大很得复杂的概念,但也可以是一个很简单的东西,就像下面使用
一样:
struct my_is_obj {
char *name;
int type;
};
可以认为struct my_is_obj就是一个对象,简单吧,但对应的实际上如何使用呢,那我们就以IIC的使用来
说明:
IIC顾名思义,在单片机编程中IIC可以说基本都会用的一个总线,比如EEPROM其接口大部分都是基于IIC,当然
也有SPI接口的。现在的音片机基本上都带有支持IIC协议的控制器,也即硬件IIC,但可能是我个人比较懒不喜欢
去读每种MCU的IIC控制器的寄存器,使用方式等等,我希望我的程序写过一次后就可以重复使用不用再写第二次,
你懒吧!所以软件形式的IIC就成了我的最爱!
程序模块化及可复用性的原则一:面像接口编程
程序模块化及可复用性的原则二:硬件的抽象接口尽量通用简单
所谓的面像接口编程,简单的理解就是在你写程序前,把所需要实现的东西看成一个具有一些数据与函数操作的集合,
并认真抽象出其函数原型,这话听着简单,可是如果要想抽象出一个比较合理且全面的函数接口谈何容易,你看LINUX
的驱动框架中那些函数指针,你就能感受到了!对于软件IIC的实现可抽象如下所示:
- struct i2c_bus {
- void (*bus_sda)(const struct i2c_bus *i2c, int level);
- void (*bus_scl)(const struct i2c_bus *i2c, int level);
- int (*read_sda)(const struct i2c_bus *i2c);
- unsigned long speed_hz;
- unsigned long ack_timeout_us;
- void *priv;
- };
[color=rgb(51, 102, 153) !important]复制代码
struct i2c_bus 里定义了IIC的底层操作函数指针及一些描述IIC的数据,因些通过struct i2c_bus结构体就能
很好把硬件相关的IO操作和上层IIC协议逻辑操作隔离开来,而具体的IIC提供的函数接口如下所示:
- void i2cbus_send_start(const struct i2c_bus *i2c);
- void i2cbus_send_stop(const struct i2c_bus *i2c);
- void i2cbus_send_noack(const struct i2c_bus *i2c);
- void i2cbus_send_ack(const struct i2c_bus *i2c);
- char i2cbus_wait_ack(const struct i2c_bus *i2c);
- void i2cbus_send_byte(const struct i2c_bus *i2c, char c);
- char i2cbus_recv_byte(const struct i2c_bus *i2c);
[color=rgb(51, 102, 153) !important]复制代码
这里给出了IIC协议里最常用的几个操作,其具体的实现如下:
- #include
- /**
- * @brief None.
- *
- * @param us
- * @Return None.
- * @NOTE None.
- */
- static void udelay(unsigned long us)
- {
- volatile unsigned long delay = 10;
- while (us) {
- for (unsigned long i = 0; i < delay; i++) {
- ;
- }
- us--;
- }
- }
- /**
- * @brief None.
- *
- * @note None.
- */
- static void i2cbus_delay(const struct i2c_bus *i2c)
- {
- volatile unsigned long us = (1000 * 1000 ) / i2c->speed_hz;
- if (us == 0) {
- us = 10;
- }
- udelay(us);
- }
- /**
- * @brief None.
- *
- * @note None.
- */
- void i2cbus_send_start(const struct i2c_bus *i2c)
- {
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 1);
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 0);
- i2cbus_delay(i2c);
- }
- /**
- * @brief sclk为高时sdat的上升沿表示“停止.
- *
- * @note None.
- */
- void i2cbus_send_stop(const struct i2c_bus *i2c)
- {
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 1);
- i2cbus_delay(i2c);
- }
- /**
- * @brief 不拉低数据线,即不给于应答.
- *
- * @note None.
- */
- void i2cbus_send_noack(const struct i2c_bus *i2c)
- {
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 1);
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- i2cbus_delay(i2c);
- }
- /**
- * @brief 拉低数据线,即给于应答.
- *
- * @param i2c
- * @return None.
- * @note None.
- */
- void i2cbus_send_ack(const struct i2c_bus *i2c)
- {
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 0); //拉低数据线,即给于应答
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- i2cbus_delay(i2c);
- }
- /**
- * @brief None.
- *
- * @param i2c
- * @return
- * @note None.
- */
- char i2cbus_wait_ack(const struct i2c_bus *i2c)
- {
- char sda;
- unsigned long ack_timeout = i2c->ack_timeout_us;
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 1); //释放数据线
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- /* 数据线未被拉低,即未收到应答 */
- while ((sda = i2c->read_sda(i2c)) && ack_timeout) {
- ack_timeout--;
- udelay(1);
- }
- /* 数据线被拉低,即收到应答 */
- return (sda ? 0 : 1);
- }
- /**
- * @brief None.
- *
- * @param i2c
- * @param c
- * @return None.
- * @note None.
- */
- void i2cbus_send_byte(const struct i2c_bus *i2c, char c)
- {
- for (char i = 0; i < 8; i++) {
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- if (c & 0x80) {
- i2c->bus_sda(i2c, 1);
- } else {
- i2c->bus_sda(i2c, 0);
- }
- c <<= 1;
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- }
- }
- /**
- * @brief None.
- *
- * @param i2c
- * @return
- * @note None.
- */
- char i2cbus_recv_byte(const struct i2c_bus *i2c)
- {
- char c = 0;
- for (char i = 0; i < 8; i++) {
- i2c->bus_scl(i2c, 0);
- i2cbus_delay(i2c);
- i2c->bus_sda(i2c, 1);
- i2cbus_delay(i2c);
- i2c->bus_scl(i2c, 1);
- i2cbus_delay(i2c);
- c <<= 1;
- c |= i2c->read_sda(i2c) ? 0x01 : 0x00;
- }
- return c;
- }
[color=rgb(51, 102, 153) !important]复制代码
有了上述的软件IIC代码,那现在就可以和实际的器件相连系了,如EEPROM。再讨论EEPROM之前,首先我们
需要注意一下EEPROM的性质,
性质1:PAGE写功能,即EEPROM器件可以一次写入一页的数据,但进行PAGE写时需要页对齐
性质2:EEPROM每次编程时需要等待10MS左右
EEPROM可以说是我写程序当中都会用到的一个器件,因此对于EEPROM的代码
我想是只写一次那以后只要COPY+C就可以了,因此对于EEPROM我们也需要进行抽象处理,下面以24LCXX为例:
- struct dev_24lcxx {
- unsigned int slave_addr;
- unsigned int cmd_rd;
- unsigned int cmd_wr;
- unsigned int page_size;
- unsigned int page_num;
- const struct i2c_bus *i2c_bus;
- void *priv;
- };
[color=rgb(51, 102, 153) !important]复制代码
对于以上的结构体成员,我想大家应该不陌生吧,如果你陌生建议去看下EEPROM手册,:-)!
const struct i2c_bus *i2c_bus;
这个成员眼熟吧,对,就是上面的软件IIC的抽象,现在看明白EEPROM是如何和IIC连系了的吧!
通过IIC的抽象,我们就彻底把EEPROM的操作和硬件分离开来了,从EEPROM的角度看EEPROM只依赖
于IIC,因此当我更改硬件时,我们只要重新赋值一个新的IIC即可。
而对于EEPROM的操作函数抽象如下:
- int dev_24lcxx_write(const struct dev_24lcxx *dev, unsigned long offset_addr,
- const char *buffer, uint32_t n_byte);
- int dev_24lcxx_read (const struct dev_24lcxx *dev, unsigned long offset_addr,
- char *buffer, uint32_t n_byte);
[color=rgb(51, 102, 153) !important]复制代码
这里说明下,以后不注明所有的函数成功则返回0,失败则返回非0。其函数的实现如下所示,
整个实现不难理解,唯一难懂的可能就是PAGE写的问题!但这不是本文的重点,你只要明白
以下实现都是通过const struct i2c_bus *i2c_bus接口来操作具体硬件的即可!
- #include
- /**
- * @brief 向EEPROM中写入一页数据,此函数不用考虑页对齐,而由调用者处理页对齐问题.
- *
- * @param dev
- * @param buffer 指向写入的缓冲区
- * @param write_addr EEPROM基地址
- * @param n_byte 写入的字节数量
- * @return
- * @note None.
- */
- static int write_page(const struct dev_24lcxx *dev, const char *buffer,
- uint16_t write_addr, uint16_t n_byte)
- {
- uint8_t addr_hi, addr_low, cmd_wr, slave_addr;
- cmd_wr = (uint8_t)dev->cmd_wr;
- slave_addr = (uint8_t)dev->slave_addr;
- addr_hi = (write_addr >> 8) & 0x00FF;
- addr_low = write_addr & 0x00FF;
- i2cbus_send_start(dev->i2c_bus);
- // Send SLAVE addr with write request
- i2cbus_send_byte(dev->i2c_bus, slave_addr | cmd_wr);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- return -1;
- }
- // Send memory addr
- i2cbus_send_byte(dev->i2c_bus, addr_hi);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- return -1;
- }
- i2cbus_send_byte(dev->i2c_bus, addr_low);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- return -1;
- }
- // Send SendData to memory addr
- for (uint16_t i = 0; i < n_byte; i++) {
- i2cbus_send_byte(dev->i2c_bus, buffer);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- return -1;
- }
- }
- i2cbus_send_stop(dev->i2c_bus);
- // 这里使用延时10MS来完成页写入操作
- OS_ERR os_err;
- OSTimeDlyHMSM(0, 0, 0, 10, 0, &os_err);
- return 0;
- }
- /**
- * @brief 向EEPROM中写入数据,并处理页对齐问题.
- *
- * @param dev
- * @param buffer 指向写入的缓冲区
- * @param write_addr EEPROM基地址
- * @param n_byte 写入的字节数量
- * @return
- * @note None.
- */
- static int write_buffer(const struct dev_24lcxx *dev, const char *buffer,
- uint16_t write_addr, uint16_t n_byte)
- {
- int r = 0;
- uint16_t n_page = 0, n_single = 0, counter = 0;
- uint16_t addr = 0;
- unsigned int page_size;
- page_size = dev->page_size;
- addr = write_addr % page_size;
- counter = page_size - addr;
- n_page = n_byte / page_size;
- n_single = n_byte % page_size;
- // 此时地址是对齐的,所以可以直接使用页写就可以
- /*!< If write_addr is page_size aligned */
- if (addr == 0) {
- /*!< If n_byte < page_size */
- if (n_page == 0) {
- /* Start writing data */
- if (write_page(dev, buffer, write_addr, n_single) < 0) {
- return -1;
- }
- } else {
- /*!< If n_byte > page_size */
- while (n_page--) {
- /* Store the number of data to be written */
- if (write_page(dev, buffer, write_addr, page_size) < 0) {
- return -1;
- }
- write_addr += page_size;
- buffer += page_size;
- }
- if (n_single != 0) {
- if (write_page(dev, buffer, write_addr, n_single) < 0) {
- return -1;
- }
- }
- }
- } else {
- /*!< If write_addr is not page_size aligned */
- /*!< If n_byte < page_size */
- if (n_page == 0) {
- /*!< If the number of data to be written is more than the remaining space
- in the current page: */
- if (n_byte > counter) {
- /*!< Write the data conained in same page */
- if (write_page(dev, buffer, write_addr, counter) < 0) {
- return -1;
- }
- /*!< Write the remaining data in the following page */
- if (write_page(dev, buffer + counter, (write_addr + counter), (n_byte - counter)) < 0) {
- return -1;
- }
- } else {
- if (write_page(dev, buffer, write_addr, n_single) < 0) {
- return -1;
- }
- }
- } else {
- /*!< If n_byte > page_size */
- n_byte -= counter;
- n_page = n_byte / page_size;
- n_single = n_byte % page_size;
- if (counter != 0) {
- if (write_page(dev, buffer, write_addr, counter) < 0) {
- return -1;
- }
- write_addr += counter;
- buffer += counter;
- }
- while (n_page--) {
- if (write_page(dev, buffer, write_addr, page_size) < 0) {
- return -1;
- }
- write_addr += page_size;
- buffer += page_size;
- }
- if (n_single != 0) {
- if (write_page(dev, buffer, write_addr, n_single) < 0) {
- return -1;
- }
- }
- }
- }
- return r;
- }
- /**
- * @brief 向EEPROM中读出数据.
- *
- * @param dev
- * @param buffer 指向读出的缓冲区
- * @param read_addr EEPROM基地址
- * @param n_byte 读出的字节数量
- * @return
- * @note None.
- */
- static int read_buffer(const struct dev_24lcxx *dev, char *buffer,
- uint16_t read_addr, uint16_t n_byte)
- {
- uint8_t addr_hi, addr_low, cmd_rd, cmd_wr, slave_addr;
- cmd_rd = (uint8_t)dev->cmd_rd;
- cmd_wr = (uint8_t)dev->cmd_wr;
- slave_addr = (uint8_t)dev->slave_addr;
- addr_hi = (read_addr >> 8) & 0x00FF;
- addr_low = read_addr & 0x00FF;
- i2cbus_send_start(dev->i2c_bus);
- // Send SLAVE addr with write request
- i2cbus_send_byte(dev->i2c_bus, slave_addr | cmd_wr);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- kprintf("i2cbus NO ack line %u", __LINE__);
- return -1;
- }
- // Send memory addr
- i2cbus_send_byte(dev->i2c_bus, addr_hi);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- kprintf("i2cbus NO ack line %u", __LINE__);
- return -1;
- }
- i2cbus_send_byte(dev->i2c_bus, addr_low);
- if (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- kprintf("i2cbus NO ack line %u", __LINE__);
- return -1;
- }
- i2cbus_send_start(dev->i2c_bus);
- i2cbus_send_byte(dev->i2c_bus, slave_addr | cmd_rd);
- while (!i2cbus_wait_ack(dev->i2c_bus)) {
- i2cbus_send_stop(dev->i2c_bus);
- kprintf("i2cbus NO ack line %u", __LINE__);
- return -1;
- }
- buffer[0] = i2cbus_recv_byte(dev->i2c_bus);
- for (uint16_t i = 1; i < n_byte; i++) {
- i2cbus_send_ack(dev->i2c_bus);
- buffer = i2cbus_recv_byte(dev->i2c_bus);
- }
- i2cbus_send_stop(dev->i2c_bus);
- return 0;
- }
- /**
- * @brief None.
- *
- * @param dev
- * @param offset_addr
- * @param buffer
- * @param n_byte
- * @return
- * @note None.
- */
- int dev_24lcxx_write(const struct dev_24lcxx *dev, unsigned long offset_addr,
- const char *buffer, uint32_t n_byte)
- {
- unsigned int page_size, page_num;
- page_size = dev->page_size;
- page_num = dev->page_num;
- if (offset_addr > ((page_num * page_size) - 1)) {
- return -1;
- }
- if (!buffer) {
- return -1;
- }
- if ((offset_addr + n_byte) > ((page_num * page_size) - 1)) {
- return -1;
- }
- return write_buffer(dev, buffer, offset_addr, n_byte);
- }
- /**
- * @brief None.
- *
- * @param dev
- * @param offset_addr
- * @param buffer
- * @param n_byte
- * @return
- * @note None.
- */
- int dev_24lcxx_read (const struct dev_24lcxx *dev, unsigned long offset_addr,
- char *buffer, uint32_t n_byte)
- {
- unsigned int page_size, page_num;
- page_size = dev->page_size;
- page_num = dev->page_num;
- if (offset_addr > ((page_num * page_size) - 1)) {
- return -1;
- }
- if (!buffer) {
- return -1;
- }
- if ((offset_addr + n_byte) > ((page_num * page_size) - 1)) {
- return -1;
- }
- return read_buffer(dev, buffer, offset_addr, n_byte);
- }
[color=rgb(51, 102, 153) !important]复制代码
如果你认真看完上述代码,或许你会有这样的疑问,怎么没有初始化函数,对的,确实没有初始化函数,
一般的程序编写方式都会有一个初始化的函数接口,这里之所以没有是因为关于初始化的问题,我也有
自己的一套方法,会开一个专题进行讨论。这里你只需知道其实硬件的初始化工作已经在这之前完成了。
有了上述的代码,接下来是要看看如何使用了,一般在项目中我会按如下方式使用:
- #include
- #include
- #define EE_ADDR 0x00
- #define EE_CMD_RD 0xA1
- #define EE_CMD_WR 0xA0
- #define EE_PAGESIZE 128
- #define EE_PAGENUM 512
- static void bus_sda(const struct i2c_bus *i2c, int level)
- {
- if (level) {
- // 硬件操作
- } else {
- // 硬件操作
- }
- }
- static void bus_scl(const struct i2c_bus *i2c, int level)
- {
- if (level) {
- // 硬件操作
- } else {
- // 硬件操作
- }
- }
- static int read_sda(const struct i2c_bus *i2c)
- {
- uint8_t c = (// 硬件操作) ? 1 : 0;
- return c;
- }
- const struct i2c_bus ee24lc512_i2cbus = {
- .bus_sda = bus_sda,
- .bus_scl = bus_scl,
- .read_sda = read_sda,
- .speed_hz = 400000,
- .ack_timeout_us = 5,
- .priv = 0,
- };
- const struct dev_24lcxx dev_ee24lc512 = {
- .slave_addr = EE_ADDR,
- .cmd_rd = EE_CMD_RD,
- .cmd_wr = EE_CMD_WR,
- .page_size = EE_PAGESIZE,
- .page_num = EE_PAGENUM,
- .i2c_bus = &ee24lc512_i2cbus,
- .priv = 0,
- };
[color=rgb(51, 102, 153) !important]复制代码
看到这儿,你应该会明白了吧,其实面象对象,也可以如此简单!有了const struct dev_24lcxx dev_ee24lc512对象,
我们就可以使用dev_24lcxx_write接口对其进行读写了!通过以上的代码演示,我们可以总结出写模块化及可复用的程序
几点原则:
1、面象接口编程
a) 即当你在写一个设备的驱动程序时,你应该首先想下提供给用户的函数接口是怎么样的,
b) 其接口应尽量通用且统一,而且接口尽量简单
2、硬件的抽象接口尽量通用简单
a) 抽象其与硬件相关的接口,也应简单明了,把这些接口和数据通过struct组合起来
或许到这里,还不能完全体现其可复用性,后续讲解中,你会慢慢发现!也许对于初学者来说
到这里已经觉得不错了,以后我也会写EEPROM的程序了,至少曾经我是这样认为的,呵呵!其实要想写
好EEPROM程序,还差的远,而EEPROM的可复用性还差的远!如果你认为你已经会了,请考虑以下问题:
1、EEPROM一般做什么使用
2、EEPROM的操作如何在多任务中使用
3、如何避免错误程序的多次误写(EEPROM有擦除次数的)
4、EEPROM如何考虑写时掉电,且如何识别错误并恢复
5、如何提高EEPROM的写效率,即那写的10MS延时
其实关于EEPROM的内容,接下来的讲述中会对EEPROM的操作进行全面的描述,
通过一个EEPROM的操作实例来阐述程序的模块化及可复用性。
|