搜索
bottom↓
回复: 88

单片机编程之 - 程序模块化及可复用性(一)

  [复制链接]

出0入0汤圆

发表于 2014-5-14 19:57:48 | 显示全部楼层 |阅读模式
本帖最后由 electrlife 于 2014-5-14 20:21 编辑

单片机编程之 - 程序模块化及可复用性(一)

关于这个贴子的原由请查看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的实现可抽象如下所示:

  1. struct i2c_bus {
  2.     void (*bus_sda)(const struct i2c_bus *i2c, int level);
  3.     void (*bus_scl)(const struct i2c_bus *i2c, int level);
  4.     int (*read_sda)(const struct i2c_bus *i2c);
  5.     unsigned long speed_hz;
  6.     unsigned long ack_timeout_us;
  7.     void *priv;
  8. };
复制代码


struct i2c_bus 里定义了IIC的底层操作函数指针及一些描述IIC的数据,因些通过struct i2c_bus结构体就能
很好把硬件相关的IO操作和上层IIC协议逻辑操作隔离开来,而具体的IIC提供的函数接口如下所示:

  1. void i2cbus_send_start(const struct i2c_bus *i2c);
  2. void i2cbus_send_stop(const struct i2c_bus *i2c);
  3. void i2cbus_send_noack(const struct i2c_bus *i2c);
  4. void i2cbus_send_ack(const struct i2c_bus *i2c);
  5. char i2cbus_wait_ack(const struct i2c_bus *i2c);
  6. void i2cbus_send_byte(const struct i2c_bus *i2c, char c);
  7. char i2cbus_recv_byte(const struct i2c_bus *i2c);
复制代码


这里给出了IIC协议里最常用的几个操作,其具体的实现如下:


  1. #include <drivers/softi2c_bus.h>

  2. /**
  3. * @brief  None.
  4. *
  5. * @param  us
  6. * @return None.
  7. * @note   None.
  8. */
  9. static void udelay(unsigned long us)
  10. {
  11.     volatile unsigned long delay = 10;

  12.     while (us) {
  13.         for (unsigned long i = 0; i < delay; i++) {
  14.             ;
  15.         }
  16.         us--;
  17.     }
  18. }

  19. /**
  20. * @brief  None.
  21. *
  22. * @note   None.
  23. */
  24. static void i2cbus_delay(const struct i2c_bus *i2c)
  25. {
  26.     volatile unsigned long us = (1000 * 1000 ) / i2c->speed_hz;

  27.     if (us == 0) {
  28.         us = 10;
  29.     }

  30.     udelay(us);
  31. }

  32. /**
  33. * @brief  None.
  34. *
  35. * @note   None.
  36. */
  37. void i2cbus_send_start(const struct i2c_bus *i2c)
  38. {
  39.     i2c->bus_scl(i2c, 0);
  40.     i2cbus_delay(i2c);
  41.     i2c->bus_sda(i2c, 1);
  42.     i2cbus_delay(i2c);

  43.     i2c->bus_scl(i2c, 1);
  44.     i2cbus_delay(i2c);
  45.     i2c->bus_sda(i2c, 0);
  46.     i2cbus_delay(i2c);
  47. }

  48. /**
  49. * @brief  sclk为高时sdat的上升沿表示“停止.
  50. *
  51. * @note   None.
  52. */
  53. void i2cbus_send_stop(const struct i2c_bus *i2c)
  54. {
  55.     i2c->bus_scl(i2c, 0);
  56.     i2cbus_delay(i2c);
  57.     i2c->bus_sda(i2c, 0);
  58.     i2cbus_delay(i2c);

  59.     i2c->bus_scl(i2c, 1);
  60.     i2cbus_delay(i2c);
  61.     i2c->bus_sda(i2c, 1);
  62.     i2cbus_delay(i2c);
  63. }

  64. /**
  65. * @brief  不拉低数据线,即不给于应答.
  66. *
  67. * @note   None.
  68. */
  69. void i2cbus_send_noack(const struct i2c_bus *i2c)
  70. {
  71.     i2c->bus_scl(i2c, 0);
  72.     i2cbus_delay(i2c);
  73.     i2c->bus_sda(i2c, 1);
  74.     i2cbus_delay(i2c);

  75.     i2c->bus_scl(i2c, 1);
  76.     i2cbus_delay(i2c);
  77.     i2cbus_delay(i2c);
  78. }

  79. /**
  80. * @brief  拉低数据线,即给于应答.
  81. *
  82. * @param  i2c
  83. * @return None.
  84. * @note   None.
  85. */
  86. void i2cbus_send_ack(const struct i2c_bus *i2c)
  87. {
  88.     i2c->bus_scl(i2c, 0);
  89.     i2cbus_delay(i2c);
  90.     i2c->bus_sda(i2c, 0);   //拉低数据线,即给于应答
  91.     i2cbus_delay(i2c);

  92.     i2c->bus_scl(i2c, 1);
  93.     i2cbus_delay(i2c);
  94.     i2cbus_delay(i2c);
  95. }

  96. /**
  97. * @brief  None.
  98. *
  99. * @param  i2c
  100. * @return
  101. * @note   None.
  102. */
  103. char i2cbus_wait_ack(const struct i2c_bus *i2c)
  104. {
  105.     char sda;
  106.     unsigned long ack_timeout = i2c->ack_timeout_us;

  107.     i2c->bus_scl(i2c, 0);
  108.     i2cbus_delay(i2c);
  109.     i2c->bus_sda(i2c, 1);   //释放数据线
  110.     i2cbus_delay(i2c);

  111.     i2c->bus_scl(i2c, 1);
  112.     i2cbus_delay(i2c);

  113.     /* 数据线未被拉低,即未收到应答 */
  114.     while ((sda = i2c->read_sda(i2c)) && ack_timeout) {
  115.         ack_timeout--;
  116.         udelay(1);
  117.     }

  118.     /* 数据线被拉低,即收到应答 */
  119.     return (sda ? 0 : 1);
  120. }

  121. /**
  122. * @brief  None.
  123. *
  124. * @param  i2c
  125. * @param  c
  126. * @return None.
  127. * @note   None.
  128. */
  129. void i2cbus_send_byte(const struct i2c_bus *i2c, char c)
  130. {
  131.     for (char i = 0; i < 8; i++) {
  132.         i2c->bus_scl(i2c, 0);
  133.         i2cbus_delay(i2c);
  134.         if (c & 0x80) {
  135.             i2c->bus_sda(i2c, 1);
  136.         } else {
  137.             i2c->bus_sda(i2c, 0);
  138.         }
  139.         c <<= 1;
  140.         i2cbus_delay(i2c);
  141.         i2c->bus_scl(i2c, 1);
  142.         i2cbus_delay(i2c);
  143.     }
  144. }

  145. /**
  146. * @brief  None.
  147. *
  148. * @param  i2c
  149. * @return
  150. * @note   None.
  151. */
  152. char i2cbus_recv_byte(const struct i2c_bus *i2c)
  153. {
  154.     char c = 0;

  155.     for (char i = 0; i < 8; i++) {
  156.         i2c->bus_scl(i2c, 0);
  157.         i2cbus_delay(i2c);
  158.         i2c->bus_sda(i2c, 1);
  159.         i2cbus_delay(i2c);

  160.         i2c->bus_scl(i2c, 1);
  161.         i2cbus_delay(i2c);
  162.         c <<= 1;
  163.         c |= i2c->read_sda(i2c) ? 0x01 : 0x00;
  164.     }

  165.     return c;
  166. }
复制代码


    有了上述的软件IIC代码,那现在就可以和实际的器件相连系了,如EEPROM。再讨论EEPROM之前,首先我们
需要注意一下EEPROM的性质,
性质1:PAGE写功能,即EEPROM器件可以一次写入一页的数据,但进行PAGE写时需要页对齐
性质2:EEPROM每次编程时需要等待10MS左右
    EEPROM可以说是我写程序当中都会用到的一个器件,因此对于EEPROM的代码
我想是只写一次那以后只要COPY+C就可以了,因此对于EEPROM我们也需要进行抽象处理,下面以24LCXX为例:

  1. struct dev_24lcxx {
  2.     unsigned int slave_addr;
  3.     unsigned int cmd_rd;
  4.     unsigned int cmd_wr;
  5.     unsigned int page_size;
  6.     unsigned int page_num;
  7.     const struct i2c_bus *i2c_bus;
  8.     void *priv;
  9. };
复制代码


对于以上的结构体成员,我想大家应该不陌生吧,如果你陌生建议去看下EEPROM手册,:-)!
const struct i2c_bus *i2c_bus;
这个成员眼熟吧,对,就是上面的软件IIC的抽象,现在看明白EEPROM是如何和IIC连系了的吧!
通过IIC的抽象,我们就彻底把EEPROM的操作和硬件分离开来了,从EEPROM的角度看EEPROM只依赖
于IIC,因此当我更改硬件时,我们只要重新赋值一个新的IIC即可。

而对于EEPROM的操作函数抽象如下:

  1. int dev_24lcxx_write(const struct dev_24lcxx *dev, unsigned long offset_addr,
  2.                      const char *buffer, uint32_t n_byte);
  3. int dev_24lcxx_read (const struct dev_24lcxx *dev, unsigned long offset_addr,
  4.                      char *buffer, uint32_t n_byte);
复制代码


这里说明下,以后不注明所有的函数成功则返回0,失败则返回非0。其函数的实现如下所示,
整个实现不难理解,唯一难懂的可能就是PAGE写的问题!但这不是本文的重点,你只要明白
以下实现都是通过const struct i2c_bus *i2c_bus接口来操作具体硬件的即可!


  1. #include <drivers/dev_24lcxx_softi2c.h>

  2. /**
  3. * @brief  向EEPROM中写入一页数据,此函数不用考虑页对齐,而由调用者处理页对齐问题.
  4. *
  5. * @param  dev
  6. * @param  buffer 指向写入的缓冲区
  7. * @param  write_addr EEPROM基地址
  8. * @param  n_byte 写入的字节数量
  9. * @return
  10. * @note   None.
  11. */
  12. static int write_page(const struct dev_24lcxx *dev, const char *buffer,
  13.                       uint16_t write_addr, uint16_t n_byte)
  14. {
  15.     uint8_t addr_hi, addr_low, cmd_wr, slave_addr;

  16.     cmd_wr = (uint8_t)dev->cmd_wr;
  17.     slave_addr = (uint8_t)dev->slave_addr;

  18.     addr_hi  = (write_addr >> 8) & 0x00FF;
  19.     addr_low =  write_addr & 0x00FF;

  20.     i2cbus_send_start(dev->i2c_bus);

  21.     // Send SLAVE addr with write request
  22.     i2cbus_send_byte(dev->i2c_bus, slave_addr | cmd_wr);

  23.     if (!i2cbus_wait_ack(dev->i2c_bus)) {
  24.         i2cbus_send_stop(dev->i2c_bus);
  25.         return -1;
  26.     }

  27.     // Send memory addr
  28.     i2cbus_send_byte(dev->i2c_bus, addr_hi);
  29.     if (!i2cbus_wait_ack(dev->i2c_bus)) {
  30.         i2cbus_send_stop(dev->i2c_bus);
  31.         return -1;
  32.     }

  33.     i2cbus_send_byte(dev->i2c_bus, addr_low);
  34.     if (!i2cbus_wait_ack(dev->i2c_bus)) {
  35.         i2cbus_send_stop(dev->i2c_bus);
  36.         return -1;
  37.     }

  38.     // Send SendData to memory addr
  39.     for (uint16_t i = 0; i < n_byte; i++) {
  40.         i2cbus_send_byte(dev->i2c_bus, buffer[i]);
  41.         if (!i2cbus_wait_ack(dev->i2c_bus)) {
  42.             i2cbus_send_stop(dev->i2c_bus);
  43.             return -1;
  44.         }
  45.     }

  46.     i2cbus_send_stop(dev->i2c_bus);

  47.     // 这里使用延时10MS来完成页写入操作
  48.     OS_ERR os_err;
  49.     OSTimeDlyHMSM(0, 0, 0, 10, 0, &os_err);

  50.     return 0;
  51. }

  52. /**
  53. * @brief  向EEPROM中写入数据,并处理页对齐问题.
  54. *
  55. * @param  dev
  56. * @param  buffer 指向写入的缓冲区
  57. * @param  write_addr EEPROM基地址
  58. * @param  n_byte 写入的字节数量
  59. * @return
  60. * @note   None.
  61. */
  62. static int write_buffer(const struct dev_24lcxx *dev, const char *buffer,
  63.                         uint16_t write_addr, uint16_t n_byte)
  64. {
  65.     int r = 0;
  66.     uint16_t n_page = 0, n_single = 0, counter = 0;
  67.     uint16_t addr = 0;

  68.     unsigned int page_size;

  69.     page_size = dev->page_size;

  70.     addr     = write_addr % page_size;
  71.     counter  = page_size - addr;
  72.     n_page   = n_byte / page_size;
  73.     n_single = n_byte % page_size;

  74.     // 此时地址是对齐的,所以可以直接使用页写就可以
  75.     /*!< If write_addr is page_size aligned  */
  76.     if (addr == 0) {
  77.         /*!< If n_byte < page_size */
  78.         if (n_page == 0) {
  79.             /* Start writing data */
  80.             if (write_page(dev, buffer, write_addr, n_single) < 0) {
  81.                 return -1;
  82.             }
  83.         } else {
  84.             /*!< If n_byte > page_size */
  85.             while (n_page--) {
  86.                 /* Store the number of data to be written */
  87.                 if (write_page(dev, buffer, write_addr, page_size) < 0) {
  88.                     return -1;
  89.                 }
  90.                 write_addr += page_size;
  91.                 buffer   += page_size;
  92.             }

  93.             if (n_single != 0) {
  94.                 if (write_page(dev, buffer, write_addr, n_single) < 0) {
  95.                     return -1;
  96.                 }
  97.             }
  98.         }
  99.     } else {
  100.         /*!< If write_addr is not page_size aligned  */
  101.         /*!< If n_byte < page_size */
  102.         if (n_page == 0) {
  103.             /*!< If the number of data to be written is more than the remaining space
  104.             in the current page: */
  105.             if (n_byte > counter) {
  106.                 /*!< Write the data conained in same page */
  107.                 if (write_page(dev, buffer, write_addr, counter) < 0) {
  108.                     return -1;
  109.                 }
  110.                 /*!< Write the remaining data in the following page */
  111.                 if (write_page(dev, buffer + counter, (write_addr + counter), (n_byte - counter)) < 0) {
  112.                     return -1;
  113.                 }
  114.             } else {
  115.                 if (write_page(dev, buffer, write_addr, n_single) < 0) {
  116.                     return -1;
  117.                 }
  118.             }
  119.         } else {
  120.             /*!< If n_byte > page_size */
  121.             n_byte  -= counter;
  122.             n_page   = n_byte / page_size;
  123.             n_single = n_byte % page_size;

  124.             if (counter != 0) {
  125.                 if (write_page(dev, buffer, write_addr, counter) < 0) {
  126.                     return -1;
  127.                 }
  128.                 write_addr += counter;
  129.                 buffer   += counter;
  130.             }

  131.             while (n_page--) {
  132.                 if (write_page(dev, buffer, write_addr, page_size) < 0) {
  133.                     return -1;
  134.                 }
  135.                 write_addr += page_size;
  136.                 buffer   += page_size;
  137.             }
  138.             if (n_single != 0) {
  139.                 if (write_page(dev, buffer, write_addr, n_single) < 0) {
  140.                     return -1;
  141.                 }
  142.             }
  143.         }
  144.     }

  145.     return r;
  146. }

  147. /**
  148. * @brief  向EEPROM中读出数据.
  149. *
  150. * @param  dev
  151. * @param  buffer 指向读出的缓冲区
  152. * @param  read_addr EEPROM基地址
  153. * @param  n_byte 读出的字节数量
  154. * @return
  155. * @note   None.
  156. */
  157. static int read_buffer(const struct dev_24lcxx *dev, char *buffer,
  158.                        uint16_t read_addr, uint16_t n_byte)
  159. {
  160.     uint8_t addr_hi, addr_low, cmd_rd, cmd_wr, slave_addr;

  161.     cmd_rd = (uint8_t)dev->cmd_rd;
  162.     cmd_wr = (uint8_t)dev->cmd_wr;
  163.     slave_addr = (uint8_t)dev->slave_addr;

  164.     addr_hi  = (read_addr >> 8) & 0x00FF;
  165.     addr_low =  read_addr & 0x00FF;

  166.     i2cbus_send_start(dev->i2c_bus);

  167.     // Send SLAVE addr with write request
  168.     i2cbus_send_byte(dev->i2c_bus, slave_addr | cmd_wr);

  169.     if (!i2cbus_wait_ack(dev->i2c_bus)) {
  170.         i2cbus_send_stop(dev->i2c_bus);
  171.         kprintf("i2cbus NO ack line %u", __LINE__);
  172.         return -1;
  173.     }

  174.     // Send memory addr
  175.     i2cbus_send_byte(dev->i2c_bus, addr_hi);
  176.     if (!i2cbus_wait_ack(dev->i2c_bus)) {
  177.         i2cbus_send_stop(dev->i2c_bus);
  178.         kprintf("i2cbus NO ack line %u", __LINE__);
  179.         return -1;
  180.     }

  181.     i2cbus_send_byte(dev->i2c_bus, addr_low);
  182.     if (!i2cbus_wait_ack(dev->i2c_bus)) {
  183.         i2cbus_send_stop(dev->i2c_bus);
  184.         kprintf("i2cbus NO ack line %u", __LINE__);
  185.         return -1;
  186.     }

  187.     i2cbus_send_start(dev->i2c_bus);

  188.     i2cbus_send_byte(dev->i2c_bus, slave_addr | cmd_rd);
  189.     while (!i2cbus_wait_ack(dev->i2c_bus)) {
  190.         i2cbus_send_stop(dev->i2c_bus);
  191.         kprintf("i2cbus NO ack line %u", __LINE__);
  192.         return -1;
  193.     }

  194.     buffer[0] = i2cbus_recv_byte(dev->i2c_bus);
  195.     for (uint16_t i = 1; i < n_byte; i++) {
  196.         i2cbus_send_ack(dev->i2c_bus);
  197.         buffer[i] = i2cbus_recv_byte(dev->i2c_bus);
  198.     }

  199.     i2cbus_send_stop(dev->i2c_bus);

  200.     return 0;
  201. }

  202. /**
  203. * @brief  None.
  204. *
  205. * @param  dev
  206. * @param  offset_addr
  207. * @param  buffer
  208. * @param  n_byte
  209. * @return
  210. * @note   None.
  211. */
  212. int dev_24lcxx_write(const struct dev_24lcxx *dev, unsigned long offset_addr,
  213.                      const char *buffer, uint32_t n_byte)
  214. {
  215.     unsigned int page_size, page_num;

  216.     page_size = dev->page_size;
  217.     page_num  = dev->page_num;

  218.     if (offset_addr > ((page_num * page_size) - 1)) {
  219.         return -1;
  220.     }

  221.     if (!buffer) {
  222.         return -1;
  223.     }

  224.     if ((offset_addr + n_byte) > ((page_num * page_size) - 1)) {
  225.         return -1;
  226.     }

  227.     return write_buffer(dev, buffer, offset_addr, n_byte);
  228. }

  229. /**
  230. * @brief  None.
  231. *
  232. * @param  dev
  233. * @param  offset_addr
  234. * @param  buffer
  235. * @param  n_byte
  236. * @return
  237. * @note   None.
  238. */
  239. int dev_24lcxx_read (const struct dev_24lcxx *dev, unsigned long offset_addr,
  240.                      char *buffer, uint32_t n_byte)
  241. {
  242.     unsigned int page_size, page_num;

  243.     page_size = dev->page_size;
  244.     page_num  = dev->page_num;

  245.     if (offset_addr > ((page_num * page_size) - 1)) {
  246.         return -1;
  247.     }

  248.     if (!buffer) {
  249.         return -1;
  250.     }

  251.     if ((offset_addr + n_byte) > ((page_num * page_size) - 1)) {
  252.         return -1;
  253.     }

  254.     return read_buffer(dev, buffer, offset_addr, n_byte);
  255. }
复制代码


     如果你认真看完上述代码,或许你会有这样的疑问,怎么没有初始化函数,对的,确实没有初始化函数,
一般的程序编写方式都会有一个初始化的函数接口,这里之所以没有是因为关于初始化的问题,我也有
自己的一套方法,会开一个专题进行讨论。这里你只需知道其实硬件的初始化工作已经在这之前完成了。
     有了上述的代码,接下来是要看看如何使用了,一般在项目中我会按如下方式使用:
     

  1. #include <drivers/softi2c_bus.h>
  2. #include <drivers/dev_24lcxx_softi2c.h>

  3.      
  4. #define EE_ADDR             0x00
  5. #define EE_CMD_RD           0xA1
  6. #define EE_CMD_WR           0xA0
  7. #define EE_PAGESIZE         128
  8. #define EE_PAGENUM          512
  9.    
  10. static void bus_sda(const struct i2c_bus *i2c, int level)
  11. {
  12.     if (level) {
  13.     // 硬件操作
  14.     } else {
  15.     // 硬件操作
  16.     }
  17. }

  18. static void bus_scl(const struct i2c_bus *i2c, int level)
  19. {
  20.     if (level) {
  21.         // 硬件操作
  22.     } else {
  23.         // 硬件操作
  24.     }
  25. }

  26. static int read_sda(const struct i2c_bus *i2c)
  27. {
  28.     uint8_t c = (// 硬件操作) ? 1 : 0;

  29.     return c;
  30. }

  31. const struct i2c_bus ee24lc512_i2cbus = {
  32.     .bus_sda        = bus_sda,
  33.     .bus_scl        = bus_scl,
  34.     .read_sda       = read_sda,
  35.     .speed_hz       = 400000,
  36.     .ack_timeout_us = 5,
  37.     .priv           = 0,
  38. };

  39. const struct dev_24lcxx dev_ee24lc512 = {
  40.     .slave_addr = EE_ADDR,
  41.     .cmd_rd     = EE_CMD_RD,
  42.     .cmd_wr     = EE_CMD_WR,
  43.     .page_size  = EE_PAGESIZE,
  44.     .page_num   = EE_PAGENUM,
  45.     .i2c_bus    = &ee24lc512_i2cbus,
  46.     .priv       = 0,
  47. };
复制代码


看到这儿,你应该会明白了吧,其实面象对象,也可以如此简单!有了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的操作实例来阐述程序的模块化及可复用性。

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。

出0入0汤圆

发表于 2014-5-14 20:13:39 | 显示全部楼层
杀了个花。

建议LZ把代码用[code]和[/code]括起来,这样“[i]”不会被解释成斜体标记、也方便大家阅读。

出0入0汤圆

 楼主| 发表于 2014-5-14 20:18:32 | 显示全部楼层
eduhf_123 发表于 2014-5-14 20:13
杀了个花。

建议LZ把代码用[code]和[/code]括起来,这样“[i]”不会被解释成斜体标记、也方便大家 ...

谢谢提醒,先前不知道这个功能,已经改过来了!呵呵!:-)

出0入0汤圆

发表于 2014-5-14 20:26:02 | 显示全部楼层
感谢楼主,先顶再看

出0入0汤圆

发表于 2014-5-14 20:40:36 | 显示全部楼层
模块化,复用性这东西,到了时间点,大家都会悟的,早期强行用吧,其实就是小孩舞大刀伤了自己;这个概念得自己去懂,就象OS一样,懂了就是懂了,不懂非要用,伤神也伤力;

就象你那贴子里另外一个朋友说的,早几年看到你的描述,只会笑笑,不以为然,现在看到深以为然,其实是因为他到了这个情景,悟了;

出0入0汤圆

发表于 2014-5-14 20:57:54 | 显示全部楼层
好帖,赞一个,顶一下。

出0入0汤圆

 楼主| 发表于 2014-5-14 21:14:10 | 显示全部楼层
kinsno 发表于 2014-5-14 20:40
模块化,复用性这东西,到了时间点,大家都会悟的,早期强行用吧,其实就是小孩舞大刀伤了自己;这个概念得 ...

说的不错,确实是需要自己悟的,我只希望能给新手起到抛砖引玉!
当然还是那句话,这些都是工具,关注产品本身也是应该多花时间的!

出0入0汤圆

发表于 2014-5-14 21:32:39 | 显示全部楼层
单片机还是直接写比较好。毕竟不是什么大的系统。不过可以作为学习操作系统的基础。

出0入0汤圆

发表于 2014-5-14 21:34:10 | 显示全部楼层
很不错.....我再一次感觉到自己是个菜鸟....

出0入0汤圆

发表于 2014-5-14 21:41:35 | 显示全部楼层
mark......

出0入0汤圆

发表于 2014-5-14 21:50:47 | 显示全部楼层
还是不太懂,估计是没到悟的时候

出0入0汤圆

发表于 2014-5-14 21:54:43 | 显示全部楼层
感觉编程到一定时间,自己会去思考这个问题。

出0入0汤圆

发表于 2014-5-14 21:54:55 | 显示全部楼层
不错,平时写代码慢慢往这方面靠了,初看写成这种参数传入的形式虽然看起来比较麻烦,一个额外的好处是避免操作全局变量,其函数的操作都是可重入的。

出0入0汤圆

发表于 2014-5-14 22:15:25 来自手机 | 显示全部楼层
从一开始便用就好,成了习惯难以改变

出0入4汤圆

发表于 2014-5-14 23:03:06 | 显示全部楼层
对内核颇有研究呀

出0入0汤圆

发表于 2014-5-14 23:10:41 来自手机 | 显示全部楼层
关注学习。

出0入0汤圆

发表于 2014-5-15 00:40:26 来自手机 | 显示全部楼层
严重关注。

出0入0汤圆

发表于 2014-5-15 05:47:03 来自手机 | 显示全部楼层
看不懂,悲剧了

出0入0汤圆

发表于 2014-5-15 07:35:01 来自手机 | 显示全部楼层
好东西  顶了

出0入0汤圆

 楼主| 发表于 2014-5-15 07:44:41 来自手机 | 显示全部楼层
sunliezhi 发表于 2014-5-14 23:03
对内核颇有研究呀

比较喜欢rtos平时一直关注这方面的东西。对于ucos-iii算是比较熟悉了,有空大家交流下。

出0入0汤圆

 楼主| 发表于 2014-5-15 07:46:28 来自手机 | 显示全部楼层
heiyuu1 发表于 2014-5-15 05:47
看不懂,悲剧了

跟着动手写一遍应该就会明白了。

出0入0汤圆

 楼主| 发表于 2014-5-15 07:51:11 来自手机 | 显示全部楼层
谢谢各位捧场,乘着现在比较闲,我争取一天发一帖。自己写时才发现把自己的想法表达出来也不是件容易的事。如果我写的不清楚可以提出,大家一起讨论。

出0入0汤圆

发表于 2014-5-15 08:15:22 来自手机 | 显示全部楼层
楼主很用心,讲的很好。能否将完整的例程工程文件传上来啊,谢谢

出0入0汤圆

发表于 2014-5-15 08:31:24 | 显示全部楼层
开江了。
听课中。

出0入0汤圆

发表于 2014-5-15 08:33:38 | 显示全部楼层
这帖子也许是论坛近半年来最精华的了.楼主超赞.

出0入0汤圆

 楼主| 发表于 2014-5-15 08:36:51 来自手机 | 显示全部楼层
tragedy 发表于 2014-5-15 08:15
楼主很用心,讲的很好。能否将完整的例程工程文件传上来啊,谢谢

谢谢,由于完整的工程里含有一些公司及个人信息需一一删除整理,等我写完这些,我会找时间整理上传的。

出0入0汤圆

发表于 2014-5-15 08:49:06 | 显示全部楼层
写得不错!
不过我一直有一个疑惑,就是类似LZ这种用结构来模拟类的方式,结构很清楚统一,但是会增加软件的开销
是不是值得,有没有更好的方法来模块化?
现在正在为程序要超限烦恼呢……

出0入0汤圆

发表于 2014-5-15 08:49:41 | 显示全部楼层
支持!太好了!

出0入296汤圆

发表于 2014-5-15 09:40:03 | 显示全部楼层
zhugean 发表于 2014-5-15 08:49
写得不错!
不过我一直有一个疑惑,就是类似LZ这种用结构来模拟类的方式,结构很清楚统一,但是会增加软件 ...

如果不需要面向对象的“覆盖”或多态,其实不必将函数指针封装在对象里面。
但如果你是要基于抽象层,比如存储器抽象层,设备抽象层之类的,函数指针
的结构体,也就是虚函数表就是必须的了。你所担心的开销其实可以具体说出来
我可以具体给你说解决方案。但不要只凭借一种模糊的畏惧感就不去尝试
否则永远迈不出这一步。

这么高水准的帖子,好久没看到了。非常赞。有一点我很赞同。这里重申下:
多任务的思想,包括多任务如何通信协作的问题与是否使用操作系统无关,
只要你要使用或者实际使用了多任务的概念,这些问题就存在。裸机存在,
OS环境也存在。只不过OS方便就方便在为我们提供了直接的工具和手段处理
多任务的这些问题,逻辑却要靠自己编写这些工具。如果你没有多任务的
思想,OS的使用会给你提供这种锻炼和培训。一旦有了这种习惯和方法论
逻辑你也能实现多任务。
至于面向接口开发,这是模块化的精髓,特点就是“用扩展替代修改”。这与多任务
其实没关系。

出0入0汤圆

发表于 2014-5-15 10:03:20 | 显示全部楼层
收藏,有空再看!!!!

出0入0汤圆

发表于 2014-5-15 10:12:22 | 显示全部楼层
东西不错,让人眼前一亮,原因就是我的程序水平距离LZ还是有一段距离

出100入101汤圆

发表于 2014-5-15 10:18:19 | 显示全部楼层
有时间慢慢看!

出0入0汤圆

发表于 2014-5-15 10:32:15 | 显示全部楼层
好东西·

出0入0汤圆

发表于 2014-5-15 10:33:02 | 显示全部楼层
好东西·

出0入0汤圆

发表于 2014-5-15 10:33:27 | 显示全部楼层
记号,收藏

出0入0汤圆

 楼主| 发表于 2014-5-15 10:55:57 | 显示全部楼层
Gorgon_Meducer 发表于 2014-5-15 09:40
如果不需要面向对象的“覆盖”或多态,其实不必将函数指针封装在对象里面。
但如果你是要基于抽象层,比 ...

版主也来捧场了,这样我更有信心写下去了!
关于OS其实我身边的同事都有不占成的,
我觉得他们都是喝着苹果榨出来的汁,却说苹果不好吃!

出0入0汤圆

发表于 2014-5-15 11:19:08 | 显示全部楼层
小谈下我这小菜的编程之路
我刚开始写代码,通篇独立的变量和函数。还有很多全局变量。也没有什么多任务的感觉。
当时只是拿着51板子,写写小的程序。一个while里就一个任务。。
直道后来想写个计算器的小程序(只有加减乘除简单的功能)。1602,按键。这时候才发现自己写代码的思路很局限。
大量if else,使逻辑关系很杂乱。于是让网找程序架构方面的知识。还有状态机什么的。。
也是这个时候看到那篇有名的‘迈向单片机工程师’的文档,相见恨晚啊。
一段模仿之后,发现写代码的感觉不一样了。
之后有机会接触了C++。算是第一次接触面向对象。
这之后,我才第一次有意识的使用结构体,把同类变量放在一起...
不过C语言还是一塌糊涂,碰到指针就蒙。。
最近开始看‘C和指针’,感觉亮堂了好多


顶楼主,希望出更多这样的好文章。

出0入0汤圆

发表于 2014-5-15 12:03:41 | 显示全部楼层
其实, 程序模块化和可复用性的 程度 跟实际项目需求是要一起考虑的.

出0入0汤圆

发表于 2014-5-15 12:12:43 来自手机 | 显示全部楼层
午休时间又看了一遍,希望楼主能更多说些细节方面的。顶顶顶顶

出0入0汤圆

发表于 2014-5-15 12:40:25 | 显示全部楼层
不错的总结,带有linux驱动的味道

出0入0汤圆

发表于 2014-5-15 12:59:48 | 显示全部楼层
kinsno 发表于 2014-5-14 20:40
模块化,复用性这东西,到了时间点,大家都会悟的,早期强行用吧,其实就是小孩舞大刀伤了自己;这个概念得 ...

说的对,到那时候就会接触到,然后靠自己去领悟。

出0入0汤圆

发表于 2014-5-15 13:24:15 | 显示全部楼层
LZ有空讲讲串口接收方面的模块化编程,我想那是极好的。

出0入0汤圆

发表于 2014-5-15 13:45:38 | 显示全部楼层
漏猪是看到linux驱动框架之后悟出来的吧。。。哈哈

出0入12汤圆

发表于 2014-5-15 13:46:51 | 显示全部楼层
学习记号备用!!

出0入0汤圆

 楼主| 发表于 2014-5-15 14:07:55 来自手机 | 显示全部楼层
tragedy 发表于 2014-5-15 12:12
午休时间又看了一遍,希望楼主能更多说些细节方面的。顶顶顶顶

谢谢支持,如果再说代码细节,可能文章就比较长了,后续可能还有好多篇,太多的代码细节我担心自己能不能写下去,再者,我写这个的主要目的是想告诉大家一种思想,一种编码模式,学会这种思想,你完全可以跳出这个编码方式,我觉得这才是最重要的。

出0入0汤圆

 楼主| 发表于 2014-5-15 14:10:40 来自手机 | 显示全部楼层
hyghyg1234 发表于 2014-5-15 13:24
LZ有空讲讲串口接收方面的模块化编程,我想那是极好的。

串口确实是比较常用的模块,但这个需要等我写完这个系列后才会考虑了。

出0入0汤圆

 楼主| 发表于 2014-5-15 14:13:39 来自手机 | 显示全部楼层
nome 发表于 2014-5-15 13:45
漏猪是看到linux驱动框架之后悟出来的吧。。。哈哈

不能说没有关系,但linux浩如烟海,我估计连门都没摸到:)。

出0入0汤圆

发表于 2014-5-15 15:21:41 | 显示全部楼层
请教
static void bus_scl(const struct i2c_bus *i2c, int level)
{
    if (level) {
        // 硬件操作
    } else {
        // 硬件操作
    }
}

const struct i2c_bus *i2不是必须的吧,有什么作用

出0入296汤圆

发表于 2014-5-15 15:26:14 | 显示全部楼层
electrlife 发表于 2014-5-15 10:55
版主也来捧场了,这样我更有信心写下去了!
关于OS其实我身边的同事都有不占成的,
我觉得他们都是喝着苹 ...

我实在太懒,这类文章写起来工作量很大,希望你坚持下去,我会帮你补充。加油哈。

出0入0汤圆

发表于 2014-5-15 17:23:17 | 显示全部楼层
已经体会到这样的好处,程序很清晰,不会混论

出0入0汤圆

发表于 2014-5-15 20:05:25 | 显示全部楼层
mark                                    

出0入0汤圆

发表于 2014-5-15 23:13:54 来自手机 | 显示全部楼层
55 找本c语言的书边看lz大作边看c语言 好多看不懂呀

出0入0汤圆

 楼主| 发表于 2014-5-16 10:46:16 | 显示全部楼层
sohappyoh 发表于 2014-5-15 15:21
请教
static void bus_scl(const struct i2c_bus *i2c, int level)
{

对于目前的代码,确实是没作用,但作为对象使用
我们一般需要知道一些对象的数据及属性,你会在
后面的一些程序中看到,如:struct nvram_chip的抽象,
为了统一风格,因此这里也定义了此形参。

出0入0汤圆

 楼主| 发表于 2014-5-16 10:47:54 | 显示全部楼层
Gorgon_Meducer 发表于 2014-5-15 15:26
我实在太懒,这类文章写起来工作量很大,希望你坚持下去,我会帮你补充。加油哈。 ...

我会坚持写下去,希望版主多多批评指正!

出0入0汤圆

 楼主| 发表于 2014-5-17 10:56:34 | 显示全部楼层
Gorgon_Meducer 发表于 2014-5-15 15:26
我实在太懒,这类文章写起来工作量很大,希望你坚持下去,我会帮你补充。加油哈。 ...

版主帮忙看看能否把 “单片机编程之 - 程序模块化及可复用性(二)”最后的更新部分更新楼主位,
可能是时间长了,我已经无法编辑之前占位的楼主位了!谢谢!

出0入296汤圆

发表于 2014-5-17 11:33:53 | 显示全部楼层
electrlife 发表于 2014-5-17 10:56
版主帮忙看看能否把 “单片机编程之 - 程序模块化及可复用性(二)”最后的更新部分更新楼主位,
可能是 ...

我没有OS板块的权限。如果你发到我的板块,以后维护会容易些。而且你这些内容严格来说也不属于OS

出0入0汤圆

发表于 2014-5-17 19:52:25 | 显示全部楼层
学习了。mark

出0入0汤圆

发表于 2014-5-18 19:24:12 | 显示全部楼层
electrlife 发表于 2014-5-14 21:14
说的不错,确实是需要自己悟的,我只希望能给新手起到抛砖引玉!
当然还是那句话,这些都是工具,关注产 ...

深有同感··

出0入0汤圆

发表于 2014-5-18 21:07:03 | 显示全部楼层
很多大程序中都有用,尤其是一些OS中,这种例子太多了,推荐一本书给大家,叫《大话设计模式》,虽然是C#语言,但面向对像的方法适用任何编程语言!

出0入0汤圆

发表于 2014-5-18 22:20:45 | 显示全部楼层
感谢楼主
勉强能看明白,略有体会。
但是没有看到继承的思想,我想法是IIC是基类,EEPROM应该继承IIC基类的,而楼主在EEPROM的程序中直接调用了IIC的操作函数。
不知道有没有说对,还请楼主指教,谢谢。

出0入0汤圆

发表于 2014-5-18 22:58:42 | 显示全部楼层
学习了!!

出0入0汤圆

发表于 2014-5-19 00:03:28 | 显示全部楼层
高质量文章 ,有空细看之

出0入0汤圆

发表于 2014-5-19 19:04:10 | 显示全部楼层
悟不到会有一种抓狂的感觉,请教楼主以下函数为什么不放到struct结构体内呢?
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);

出0入0汤圆

 楼主| 发表于 2014-5-20 20:17:58 | 显示全部楼层
yixin1851 发表于 2014-5-19 19:04
悟不到会有一种抓狂的感觉,请教楼主以下函数为什么不放到struct结构体内呢?
void i2cbus_send_start(cons ...

这是一个软件IIC的模块,也就是说IIC的一些时序是通用的,我把这些时序做为一个函数并封装起来,
这些就是IIC的通用函数接口,而软件IIC也是需要硬件来支持的,所以需要把IIC与硬件联系起来,因此
对IIC做一个硬件层的抽象,所以就有了struct i2c_bus,这个结构体作用是把软件IIC函数接口需要的一些硬件
操作函数指针与需要的数据进行封装。

出0入10汤圆

发表于 2014-5-22 06:58:47 | 显示全部楼层
本帖最后由 10xjzheng 于 2014-5-22 07:00 编辑
electrlife 发表于 2014-5-16 10:46
对于目前的代码,确实是没作用,但作为对象使用
我们一般需要知道一些对象的数据及属性,你会在
后面的一 ...

楼主写得很好,楼主可否再清晰点说下这个问题。
1.为什么不用用到的形参也写进了,同样不能理解?例如static void bus_scl(const struct i2c_bus *i2c, int level)的const struct i2c_bus *i2c
2.为什么定义结构体类型dev_24lcxx的时候,要把i2c_bus也写进入,感觉是两个东西,有必要写在一起吗?
研究了一下你的文章,准备也好好学下,谢谢楼主,楼主好好坚持,把这系列的帖子一直写下去,好让我们这些菜鸟学点东西。

出0入76汤圆

发表于 2014-5-22 14:06:30 | 显示全部楼层
提一下,像A家的EEPROM,一般页擦写时间在MAX=5ms左右的样子。

有很长一段时间没有看到有深度,有含金量的大作了, 对LZ的分享精神表示感谢。
同时也更加期待后续能看到,关于面向对象 模块化封装的原则、技巧和实用方法,以及嵌入式软件架构, 设计思想等方面大作。

注: 傻孩子大侠在这些方面也写了很多优秀的文章和大作, 可能是我还比较菜,在阅读他的有些大作时,觉得有些费劲,一时还没能完全领悟他的思想。

出0入0汤圆

发表于 2014-5-30 13:30:11 | 显示全部楼层
写的很好。强烈要求坛主将该篇置精华!!!

出60入0汤圆

发表于 2014-5-30 17:44:52 来自手机 | 显示全部楼层
写得好,先mark,回去细看

出0入0汤圆

发表于 2014-6-6 08:53:16 | 显示全部楼层
好贴,是基础要掌握的呀,学习了,多谢楼主!

出0入0汤圆

发表于 2014-6-18 16:52:24 | 显示全部楼层
好强大,学习了

出0入4汤圆

发表于 2014-6-20 16:39:35 | 显示全部楼层
推荐封装,封装思想好。
不封装也成,代码大了就觉得模块封装下其实容易管理、维护

出0入0汤圆

发表于 2014-8-28 08:43:07 | 显示全部楼层
精辟,关键词,对象、复用

出0入0汤圆

发表于 2014-9-13 18:52:46 | 显示全部楼层
已偿试用在产品中

出0入0汤圆

发表于 2016-6-15 17:55:01 | 显示全部楼层
好帖,赶紧看2去。

出0入0汤圆

发表于 2016-6-16 11:12:33 | 显示全部楼层
挖坟贴啊。

出0入0汤圆

发表于 2016-12-2 10:33:09 | 显示全部楼层
膜拜大神~ 楼主“初始化”的方法有放出来么?

出0入0汤圆

发表于 2016-12-2 10:45:43 | 显示全部楼层
赞一个,谢谢分享!

出0入0汤圆

发表于 2016-12-5 09:39:44 | 显示全部楼层
顶起,好东西,慢慢也悟到模块化和复用的重要性了

出0入0汤圆

发表于 2016-12-13 16:13:44 | 显示全部楼层
代码思想很好,高度模块化。

出0入0汤圆

发表于 2016-12-13 18:32:13 | 显示全部楼层
这个思想很不错 ,我也在一直尝试着写这样的通用的代码,这样项目越做越轻松。

出0入0汤圆

发表于 2016-12-20 10:54:32 | 显示全部楼层
收藏,有时间在细看

出0入0汤圆

发表于 2016-12-20 10:54:48 | 显示全部楼层
收藏,有时间再细看。

出0入0汤圆

发表于 2016-12-21 01:01:55 | 显示全部楼层
留个脚印

出0入0汤圆

发表于 2017-1-13 09:43:22 | 显示全部楼层
我发现写到后面都有这个思路了,就是面向对像~

出0入0汤圆

发表于 2017-1-13 09:58:13 | 显示全部楼层
写得不错,看得出楼主也很用心,有好的心得多多分享。

出0入0汤圆

发表于 2017-1-13 10:16:41 | 显示全部楼层
面向接口与面向对象的好处真是不言而喻啊。用过了,才知道架构的清晰和后期维护上的便利。

出0入0汤圆

发表于 2017-9-8 18:48:22 | 显示全部楼层
我觉得EEPROM的抽象层里不应该再用到IIC的接口函数了,而是直接操作EEPROM结构体里的ICC结构体,
比如
static int write_page(const struct dev_24lcxx *dev, const char *buffer, uint16_t write_addr, uint16_t n_byte)
中的
i2cbus_send_start(dev->i2c_bus)
是不是这么说?请允许菜鸟愚昧一下

出0入8汤圆

发表于 2017-10-24 15:00:32 | 显示全部楼层
dodo555 发表于 2017-9-8 18:48
我觉得EEPROM的抽象层里不应该再用到IIC的接口函数了,而是直接操作EEPROM结构体里的ICC结构体,
比如
stat ...

说说我的看法,两种写法是有本质区别的,我觉得更多是个人风格吧,哈哈


楼主的原则之一是“硬件的抽象接口尽量通用简单”。。。

所以我觉得楼主可能的准则或风格是这样的:对一个模块来说,是由抽象的结构体和操作抽象的函数集组成的。


如果我来写,应该就是IIC结构体内带函数表了,哈哈。。。


突然发现这是几年前的帖子了,这么好的帖子为啥以前没看到?!
还更新不
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-3-29 16:14

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表